---
title: Send custom properties to ODP
slug: configured-commerce/docs/send-custom-properties-to-odp
docTags: 
createdAt: 2026-08-13T10:46:19.000Z
---

Extend your Optimizely Configured Commerce data by sending custom properties to Optimizely Data Platform (ODP). This lets you enrich customer profiles, events, and transactions with additional attributes, delivered through real-time sends or scheduled integration jobs.

Configured Commerce supports two pipelines for pushing data into ODP:

- Real‑time transmission
- Batch integration jobs (for historical backfills)

This article explains how both pipelines work, where you can wire in custom property hooks, and how to implement them cleanly.

## How sending data works

### Real-time

1. An entity change generates one or more change IDs.
2. The system runs a base SQL query for those IDs, shaping them into the ODP payload.
3. It calls your `IOdpDataCustomizationService<T>.GetDataCustomization(...)` to merge extra fields.
4. The system posts the enriched records to the ODP REST API in configurable batch sizes.

### Integration jobs

1. A scheduled job runs a full SQL extract of all target data.
2. The system writes results as CSV and uploads them to an Amazon S3 bucket.
3. ODP ingests the CSV file asynchronously.

You can inject custom properties in two ways without altering the base SQL, using one of the following:

- Static preload (Mode A)
- Dynamic factory (Mode B)

## Export enrichment mechanisms (Bulk export)

### Mode A: Static preload

- **Entry point** – Override `GetCustomizedDataQuery(filter)` in your postprocessor.
- **Trigger** – Returns a non-null `IDictionary<Guid, IDictionary<string,string>>`.
- **Behavior** – Platform merges your per-GUID dictionary into each CSV row. The system replaces missing keys with `DBNull.Value`.
- **Best for** – Fast lookup on moderate-sized data (fits in memory).

```csharp
protected override IDictionary<Guid, IDictionary<string,string>> GetCustomizedDataQuery(
    CustomizationFilterParameter? filter)
```

**Example**

```csharp
public class ProductOdpDataCustomizationService
    : IOdpDataCustomizationService<Product>
{
    private static readonly string[] PropertyNames = { "anyProperty1", "anyProperty2" };
    private readonly IUnitOfWorkFactory factory;

    public IDictionary<Guid, IDictionary<string,string>> GetDataCustomization(
        CustomizationFilterParameter? filter)
    {
        var result = new Dictionary<Guid, IDictionary<string,string>>();

        var uow = factory.GetUnitOfWork();
        var props = uow.GetRepository<CustomProperty>()
            .GetTableAsNoTracking()
            .Where(cp => cp.ParentTable == "Product"
                      && cp.ModifiedOn >= filter.From
                      && PropertyNames.Contains(cp.Name))
            .Select(cp => new { cp.ParentId, cp.Name, cp.Value });

        foreach (var row in props)
        {
            if (!result.TryGetValue(row.ParentId, out var dict))
            {
                dict = new Dictionary<string,string>(StringComparer.OrdinalIgnoreCase);
                result[row.ParentId] = dict;
            }
            dict[row.Name] = row.Value ?? string.Empty;
        }
        return result;
    }
}
```

### Mode B: Dynamic factory

Used only if `GetCustomizedDataQuery` returns `null`.

:::hint{type="warning"}
**🚧 Important**
You must create a copy of `JobPostprocessorBaseOdpExport` and the base postprocessor for your entity, such as `JobPostprocessorProductOdpExport`, before using this method to extend Configure Commerce.
:::

- **Schema** – `GetAdditionalColumnNames()` returns a list of extra column names.
- **Values** – `GetAdditionalDataFactory()` returns a `Func<Guid, object[]>` invoked per row.
- **Behavior** – Positionally maps array values to column names; short arrays will be trailing `DBNull.Value`.
- **Best for** – Compute-heavy fields or very large datasets (minimal memory).

```csharp
protected override IReadOnlyCollection<string> GetAdditionalColumnNames();
protected override Func<Guid, object[]> GetAdditionalDataFactory();
```

**Example**

```csharp
public class JobPostprocessorCustomProductOdpExport
    : JobPostprocessorProductOdpExport
{
    private static readonly string[] Columns = { "anyProperty1", "anyProperty2" };
    private readonly IUnitOfWork uow;

    protected override IDictionary<Guid, IDictionary<string,string>> GetCustomizedDataQuery(...) 
        => null;

    protected override IReadOnlyCollection<string> GetAdditionalColumnNames() 
        => Columns;

    protected override Func<Guid, object[]> GetAdditionalDataFactory() 
        => productId => {
            var props = uow.GetRepository<CustomProperty>()
                .GetTableAsNoTracking()
                .Where(cp => cp.ParentTable=="Product"
                          && cp.ParentId==productId
                          && Columns.Contains(cp.Name))
                .ToList();
            var dict = props.ToDictionary(cp=>cp.Name, cp=>cp.Value ?? string.Empty,
                                         StringComparer.OrdinalIgnoreCase);
            return new object[]
            {
                dict.GetValueOrDefault("anyProperty1",""),
                dict.GetValueOrDefault("anyProperty2","")
            };
        };
}
```

### Comparison

| Criterion         | Static preload                   | Dynamic factory                      |
| ----------------- | -------------------------------- | ------------------------------------ |
| Entry point       | `GetCustomizedDataQuery(filter)` | `GetAdditionalColumnNames` + factory |
| Memory footprint  | Preloads all extra data          | Computes per row                     |
| Column uniformity | Missing replaced by `DBNull`     | Fixed schema                         |
| Best for          | Fast lookup of known values      | Expensive or huge data sets          |

## Developer extensibility

Define custom fields for both real-time and batch without touching base SQL.

```csharp
public interface IOdpDataCustomizationService<T>
    : IDependency, IExtension
    where T : class, IBusinessObject
{
    IDictionary<Guid, IDictionary<string,string>>? GetDataCustomization(
        CustomizationFilterParameter? filter);
}
```

### CustomizationFilterParameter

- **IdFilter** – Optional `ICollection<Guid>` to enrich only specific IDs
- **From/To** – Inclusive timestamp bounds

### Integration points

- **Real-time** – `OdpUpdatedEntityEventService` invokes `GetDataCustomization` for changed IDs.
- **Batch export** – `JobPostprocessorBaseOdpExport` invokes it before CSV generation.

## Where customization hooks run

### Bulk export flow

1. Uses SQL to read base rows.
2. Applies customization.
   1. **Static** – `GetCustomizedDataQuery(filter)`
   2. **Dynamic** – `GetAdditionalColumnNames()` + `GetAdditionalDataFactory()`
3. Writes enriched CSV then uploads to S3 and ODP ingestion.

### Real-time API sync

1. Detects entity changes then collects change IDs.
2. Reruns base SQL snapshot for those IDs.
3. Merges extra fields with `IOdpDataCustomizationService`.
4. Batches client‐side and `POST` to ODP API.
