> ## Documentation Index
> Fetch the complete documentation index at: https://docs.apostra.com/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.apostra.com/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>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.