> ## Documentation Index
> Fetch the complete documentation index at: https://docs.interchange.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Save catalog transform

> Define how catalog items fan out into campaign groups, budgets, and creative prompts

`PUT /api/v2/buyer/advertisers/{advertiserId}/catalogs/{catalogId}/transform`

Saves the **active transform** for a catalog: a deterministic rule set that maps raw feed items into campaign groups, budget hints, and creative-asset prompts. Saving a new transform archives the previous active one — a catalog has exactly one active transform at a time.

## How catalog activation works

A transform has three parts, applied in order:

1. **Derived fields** (`transform.fields`) — compute new fields from raw item data. Each field pulls from a `source` dot-path (e.g. `location.city`), a `template` of `{{field}}` placeholders, or a `default`, and can be normalized with a `transform` such as `slug` or `title_case`. Derived fields feed the grouping and template rules below.
2. **Campaign grouping and budget** (`transform.campaign`) — fan the items out into campaign groups. `groupBy` names the fields to group on (usually derived fields such as `city`); `nameTemplate`/`keyTemplate` label each group; `budget` sets a spend hint per group.
3. **Creative-asset prompts** (`transform.creativeAssets`) — define reusable creative generation prompts. Each entry has a `promptTemplate`, a `keyTemplate` cache key (assets sharing a key reuse generated media), and an optional `groupBy` to fan out unique prompts.

The **lifecycle** across the four catalog-activation operations:

1. **Save transform** (this operation) — persist the active transform.
2. [**Preview activation**](/v2/buyer/advertisers/tasks/preview-catalog-activation) — dry-run the plan to see exactly what would be created, or pass `save: true` to persist it.
3. [**Execute activation**](/v2/buyer/advertisers/tasks/execute-catalog-activation) — materialize the plan into campaign, creative-generation, and seller-syndication jobs.
4. [**Refresh catalog**](/v2/buyer/advertisers/tasks/refresh-catalog) — pull the latest feed and, optionally, re-run activation.

**Worked example.** A catalog of 6 products, each with a raw `city`, is transformed with a derived `city` field, `campaign.groupBy: ["city"]`, and `nameTemplate: "Acme - {{city}}"`. Preview fans the 6 items out into 3 campaign groups — `Acme - Austin`, `Acme - Denver`, `Acme - Portland` — each with 2 items.

You can build and run the whole flow in the UI under **Data sources → Catalogs** (**Preview activation**, **Refresh feed**, **Refresh and activate**).

## Request

```bash curl theme={null}
curl -X PUT https://api.interchange.io/api/v2/buyer/advertisers/12345/catalogs/summer-sale-2026/transform \
  -H "Authorization: Bearer $SCOPE3_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "City fan-out",
    "transform": {
      "fields": [
        { "key": "city", "source": "location.city" },
        { "key": "citySlug", "source": "location.city", "transform": "slug" }
      ],
      "campaign": {
        "groupBy": ["city"],
        "keyTemplate": "acme-{{citySlug}}",
        "nameTemplate": "Acme - {{city}}",
        "budget": { "mode": "fixed", "amount": 100, "currency": "USD" }
      },
      "creativeAssets": [
        {
          "keyTemplate": "acme-city-{{citySlug}}",
          "promptTemplate": "A vibrant product ad set in {{city}}",
          "groupBy": ["city"]
        }
      ]
    }
  }'
```

## Parameters

| Field                                       | Type          | Required | Notes                                                                                                                   |
| ------------------------------------------- | ------------- | -------- | ----------------------------------------------------------------------------------------------------------------------- |
| `advertiserId`                              | string (path) | Yes      | Unique identifier for the advertiser                                                                                    |
| `catalogId`                                 | string (path) | Yes      | Buyer-assigned `catalog_id` or the platform `catalogId` from [list catalogs](/v2/buyer/advertisers/tasks/list-catalogs) |
| `name`                                      | string        | No       | Display name for the transform (1–255 chars). Defaults to `Default transform`                                           |
| `transform`                                 | object        | Yes      | The transform definition (three parts below)                                                                            |
| `transform.fields`                          | array         | No       | Derived fields. Default `[]`                                                                                            |
| `transform.fields[].key`                    | string        | Yes      | Derived field key. Must start with a letter and contain only letters, numbers, or underscores                           |
| `transform.fields[].source`                 | string        | No       | Dot-path into the raw item, e.g. `location.city`                                                                        |
| `transform.fields[].template`               | string        | No       | Template using `{{field}}` placeholders from raw or derived fields                                                      |
| `transform.fields[].default`                | any           | No       | Fallback when `source`/`template` resolve empty                                                                         |
| `transform.fields[].transform`              | enum          | No       | Value normalization: `string`, `lowercase`, `uppercase`, `title_case`, `slug`                                           |
| `transform.campaign`                        | object        | No       | Campaign grouping and budget                                                                                            |
| `transform.campaign.groupBy`                | array         | Yes\*    | Field references to group on (≥1). \*Required when `campaign` is present                                                |
| `transform.campaign.keyTemplate`            | string        | No       | Campaign group key template                                                                                             |
| `transform.campaign.nameTemplate`           | string        | No       | Human-readable campaign name template                                                                                   |
| `transform.campaign.budget`                 | object        | No       | Budget rule (see below)                                                                                                 |
| `transform.creativeAssets`                  | array         | No       | Creative-asset prompt definitions. Default `[]`                                                                         |
| `transform.creativeAssets[].keyTemplate`    | string        | Yes      | Stable cache key template; assets with the same key reuse generated media                                               |
| `transform.creativeAssets[].promptTemplate` | string        | Yes      | Prompt template for the creative generation path                                                                        |
| `transform.creativeAssets[].groupBy`        | array         | No       | Field references to fan out unique prompts. Default `[]`                                                                |

Each `fields[]` entry must define at least one of `source`, `template`, or `default`.

**Budget rule** (`transform.campaign.budget`) is a tagged union on `mode`:

| `mode`      | Fields                                          | Meaning                                              |
| ----------- | ----------------------------------------------- | ---------------------------------------------------- |
| `none`      | —                                               | No budget hint                                       |
| `fixed`     | `amount`, `currency` (default `USD`)            | Same fixed budget per campaign group                 |
| `per_item`  | `amountPerItem`, `currency`                     | Budget scales with item count in the group           |
| `field_sum` | `field`, `multiplier` (default `1`), `currency` | Sum a numeric field across items, times a multiplier |

## Response

```json theme={null}
{
  "transform": {
    "transformId": "10",
    "catalogId": "summer-sale-2026",
    "platformCatalogId": "14",
    "name": "City fan-out",
    "status": "active",
    "definitionHash": "a1b2c3…",
    "definition": {
      "fields": [
        { "key": "city", "source": "location.city" },
        { "key": "citySlug", "source": "location.city", "transform": "slug" }
      ],
      "campaign": {
        "groupBy": ["city"],
        "keyTemplate": "acme-{{citySlug}}",
        "nameTemplate": "Acme - {{city}}",
        "budget": { "mode": "fixed", "amount": 100, "currency": "USD" }
      },
      "creativeAssets": [
        {
          "keyTemplate": "acme-city-{{citySlug}}",
          "promptTemplate": "A vibrant product ad set in {{city}}",
          "groupBy": ["city"]
        }
      ]
    },
    "createdAt": "2026-06-07T12:00:00Z",
    "updatedAt": "2026-06-07T12:00:00Z"
  }
}
```

The saved transform becomes `active`; the previously active transform (if any) is set to `archived`.

## Errors

* `400 VALIDATION_ERROR` — the transform fails schema validation (e.g. a `fields[]` entry defines none of `source`/`template`/`default`, or `campaign.groupBy` is empty).
* `404 NOT_FOUND` — `advertiserId` or `catalogId` does not exist or is not visible to the authenticated account.

See [Errors](/v2/reference/errors) for the full error contract.

## Related

<CardGroup cols={2}>
  <Card title="Preview activation" href="/v2/buyer/advertisers/tasks/preview-catalog-activation" icon="eye">
    Dry-run the campaign/creative fan-out
  </Card>

  <Card title="Execute activation" href="/v2/buyer/advertisers/tasks/execute-catalog-activation" icon="play">
    Materialize the plan into jobs
  </Card>

  <Card title="Refresh catalog" href="/v2/buyer/advertisers/tasks/refresh-catalog" icon="arrow-rotate-right">
    Pull the latest feed and re-activate
  </Card>

  <Card title="Sync catalogs" href="/v2/buyer/advertisers/tasks/sync-catalogs" icon="arrows-rotate">
    Push catalog feeds and items
  </Card>
</CardGroup>
