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

# Delivering First-Party Audiences to The Trade Desk via the API

> Upload an audience to Narrative and deliver it into an advertiser's own Trade Desk seat over the API

This guide shows you how to build an unattended integration that uploads an audience to Narrative and delivers it into one advertiser's own seat at The Trade Desk (TTD). A person signs in once to create an API key, and the integration uses that key from then on.

First-party delivery needs no taxonomy and no review by TTD. To list an audience in TTD's marketplace instead, see [Third-party delivery via the API](/guides/connectors/the-trade-desk/third-party-delivery-api.md). [First-party and third-party delivery](/reference/connectors/the-trade-desk.md#overview) compares the two models.

Before you start, complete the [prerequisites](/guides/connectors/the-trade-desk/setting-up-api-access.md#prerequisites) and [set up API access](/guides/connectors/the-trade-desk/setting-up-api-access.md#set-up-api-access).

## 1. Create the dataset

Your schema decides whether The Trade Desk will accept the audience, so settle it before you upload anything.

### Required columns

`first_party` and `third_party_v2` take the same schema. The dataset needs at least one of these identifier columns:

| Column | Shape of each value |
| - | - |
| `sha256_hashed_email` | `{"value": ...}` |
| `hashed_email` | `{"value": ..., "type": "..."}` |
| `unified_id_2` | `{"value": ...}` |
| `ttd_id` | `{"value": ...}` |
| `apple_idfa` | `{"value": ...}` |
| `android_advertising_id` | `{"value": ...}` |
| `mobile_id_unique_identifier` | `{"value": ...}` |
| `maid` | A string |
| `telephone_number` | A string |
| `narrative_id` | `{"context": "...", "value": ...}` |

Each identifier column is named after its Rosetta Stone attribute, and the connector matches on that column name. A column holds either a plain string or an object. In the dataset schema, declare a string column with `"type": "string"`, and declare an object column with `"type": "object"` and its properties:

```json theme={null}
{
  "type": "object",
  "properties": {
    "<string_attribute>": { "type": "string" },
    "<object_attribute>": {
      "type": "object",
      "properties": { "<property>": { "type": "<type>" } }
    }
  }
}
```

Send an object column as an object in each row:

```json theme={null}
{"<string_attribute>": "...", "<object_attribute>": {"<property>": "..."}}
```

See your connector's reference page for the attributes it accepts and their exact shapes.

A row with one identifier looks like this:

```json theme={null}
{"sha256_hashed_email": {"value": "<64 hex characters>"}}
```

See [Supported identifiers](/reference/connectors/the-trade-desk.md#supported-identifiers) for how The Trade Desk matches each identifier.

### Check the schema first

The connector's validation endpoint takes the schema you plan to use and answers `200` with an empty body when The Trade Desk would accept it. It needs no authorization header, and `id` can be any number:

```bash theme={null}
curl -X POST https://thetradedesk.narrativeconnectors.com/validations/dataset-schema \
  -H "Content-Type: application/json" \
  -d '{
    "id": 1,
    "schema": {
      "type": "object",
      "properties": {
        "sha256_hashed_email": {
          "type": "object",
          "properties": { "value": { "type": "string" } }
        }
      }
    }
  }'
# → HTTP 200
```

A schema with no recognized identifier comes back `400`, with one entry for each identifier the connector looked for:

```json theme={null}
{
  "title": "Dataset: Invalid Schema TTD",
  "status": 400,
  "detail": "Invalid Schema",
  "debug": {
    "valid": false,
    "details": [
      { "errors": { "required": "required property 'android_advertising_id' not found" }, ... },
      ...
    ]
  }
}
```

### Create it

```bash theme={null}
curl -X POST https://api.narrative.io/datasets \
  -H "Authorization: Bearer $NIO_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "q3_in_market_auto",
    "display_name": "Q3 In-Market Auto",
    "description": "Audience for The Trade Desk",
    "write_mode": "overwrite",
    "schema": {
      "file_config": { "type": "json" },
      "type": "object",
      "properties": {
        "sha256_hashed_email": {
          "type": "object",
          "display_name": "SHA-256 Hashed Email",
          "properties": {
            "value": { "type": "string", "display_name": "Value" }
          }
        }
      }
    }
  }'
```

`write_mode` is `overwrite` or `append`. `file_config.type` is `json` for JSON Lines, `parquet`, or `flat` for CSV.

Record the dataset `id` from the response.

> [!NOTE]
>
> You do not need to supply UID2. The connector generates UID2 tokens from hashed email addresses automatically.

## 2. Activate the dataset

```bash theme={null}
curl -X POST https://api.narrative.io/datasets/{dataset_id}/activate \
  -H "Authorization: Bearer $NIO_API_TOKEN"
```

Activation locks the schema. You cannot change it afterward, so activate only once the shape is settled.

## 3. Upload and ingest your file

Each line of the file is one row that matches the schema you declared in [step 1](#1-create-the-dataset). For the columns The Trade Desk accepts and their shapes, see [Supported identifiers](/reference/connectors/the-trade-desk.md#supported-identifiers) and [Required columns](#required-columns).

You can load a file into a dataset over the API in two ways:

* **Signed-URL upload.** Your integration uploads one file of up to 3 GB, then asks Narrative to ingest it.
* **Managed S3 bucket.** You write files to an S3 bucket that Narrative manages, and Narrative ingests each batch on its own. Use a managed bucket for files larger than 3 GB, or for files that another system delivers on a schedule.

### Choose a file format

The dataset's `file_config.type` sets the format of every file you load into it. Parquet and JSON Lines both hold object columns, which nest properties inside one column:

* **Parquet** (`parquet`) is the most compatible format for connector datasets. It stores nested struct columns and their types natively, and Narrative matches columns to the schema by name at every level.
* **JSON Lines** (`json`) holds one JSON object per line. Narrative matches each nested object to the schema by name.

CSV datasets (`flat`) hold only scalar columns, so they can't carry object columns.

### Upload a file with a signed URL

Request an upload URL, then send the file straight to storage:

```bash theme={null}
curl -X POST https://api.narrative.io/uploads/my-file.jsonl \
  -H "Authorization: Bearer $NIO_API_TOKEN"
# → {"path": "<UPLOAD_PATH>", "url": "https://...", "expiry": "..."}

curl -X PUT "<url from the response>" --upload-file ./my-file.jsonl
```

The upload URL is valid for 30 minutes and carries its own signature, so send no authorization header with the `PUT`.

> [!WARNING]
>
> Keep the `path` from the response. Narrative assigns its own storage path, which does not match the name you requested, and the ingest request needs Narrative's path rather than yours.

Then ingest the file into the dataset, passing that `path` as `source_file`:

```bash theme={null}
curl -X POST https://api.narrative.io/datasets/{dataset_id}/upload \
  -H "Authorization: Bearer $NIO_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"source_file": "<UPLOAD_PATH>"}'
```

Ingestion runs in the background. Watch the record count on the dataset to know when it has finished:

```bash theme={null}
curl -s https://api.narrative.io/datasets/{dataset_id} \
  -H "Authorization: Bearer $NIO_API_TOKEN"
# → .stats.active_dataset_stored_records
```

The count moves from zero to your row count, typically within a couple of minutes. Each ingested file adds a new snapshot to the dataset, and every active connection on the dataset delivers that snapshot.

### Write files to a managed S3 bucket

A managed bucket is an S3 bucket that Narrative creates for your company. You write each batch of files into its own folder under the dataset's path in the bucket, then write an empty `_NIO_COMMIT` file into that batch folder. Narrative ingests every file in the batch folder when the commit file appears, so you make no upload or ingest request. A file can be as large as S3 accepts.

See [Ingesting Files from a Managed S3 Bucket](/guides/ingestion/ingesting-files-from-a-managed-s3-bucket.md) to create the bucket, grant your AWS account access, and lay out the folders.

## 4. Confirm The Trade Desk accepts the dataset

Before you create a connection, ask Narrative which connector interfaces the dataset satisfies:

```bash theme={null}
curl -s https://api.narrative.io/datasets/{dataset_id}/interfaces \
  -H "Authorization: Bearer $NIO_API_TOKEN"
# → {"dataset_id": 12345,
#    "accepted": [{"app_id": <APP_ID>, "interface_id": "<INTERFACE_ID>"}, ...],
#    "errors": [{"app_id": <APP_ID>, "interface_id": "<INTERFACE_ID>", "details": {...}}, ...]}
```

The response checks the dataset's schema against every interface of the connectors your company has installed, and sorts the results into two lists:

* `accepted` lists each interface you can connect the dataset to, by the connector's `app_id` and the `interface_id`.
* `errors` lists each interface the schema does not satisfy. Its `details` hold the reason, such as `"required property '<column>' not found"`.

Add `?tags=<tag>` to check only the interfaces that carry that tag.

When the interface you want is under `errors`, the dataset's schema doesn't meet what the interface needs, for example a missing column or property. Activation locks the schema, so create a new dataset that fixes what the error names.

For The Trade Desk, look for `"app_id": 11` in `accepted`, under the interface your delivery model uses: `first_party` for first-party, `third_party_v2` for third-party. See [Delivery interfaces](/reference/connectors/the-trade-desk.md#delivery-interfaces) for what each interface does.

```json theme={null}
{
  "dataset_id": 12345,
  "accepted": [
    { "app_id": 11, "interface_id": "first_party" },
    { "app_id": 11, "interface_id": "third_party_v2" },
    ...
  ],
  "errors": [ ... ]
}
```

## 5. Find your connector profile

Your connector profile holds your Brand ID and Sub-Provider ID, and every delivery names it. The connector's `/profiles` endpoint returns it:

```bash theme={null}
curl -s https://thetradedesk.narrativeconnectors.com/profiles \
  -H "Authorization: Bearer $NIO_API_TOKEN"
```

```json theme={null}
{
  "records": [
    {
      "id": "<profile_id>",
      "company_id": 1234,
      "name": "The Trade Desk Connector Profile",
      "description": null,
      "status": "enabled",
      "profile": {
        "brand_id": "<your Brand ID>",
        "sub_provider_id": "<your Sub-Provider ID>",
        "company_name": "<your company name>"
      }
    }
  ]
}
```

Record the profile `id`. Its `status` must be `enabled`. `GET /profiles/{profile_id}` returns the same record on its own. If you have no profile yet, create one in the Narrative UI under **Objects → Installed Apps → Trade Desk Connector** and enter your Brand ID there.

## 6. Register the advertiser

Register each receiving advertiser once. The registration holds the credentials the connector uses to deliver into that advertiser's seat.

```bash theme={null}
curl -X POST https://thetradedesk.narrativeconnectors.com/advertisers \
  -H "Authorization: Bearer $NIO_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme Motors",
    "ttd_id": "<their TTD advertiser id>",
    "secret": "<their secret key>"
  }'
```

The response is `200` with the registration:

```json theme={null}
{
  "id": "<registration_id>",
  "company_id": 1234,
  "created_at": "2026-10-03T15:00:00Z",
  "name": "Acme Motors",
  "ttd_id": "<their TTD advertiser id>",
  "updated_at": "2026-10-03T15:00:00Z",
  ...
}
```

`ttd_id` is the advertiser's ID in The Trade Desk. You use the same value in the next step. `id` is Narrative's identifier for the registration, which you use only to read, change, or remove it. See [Managing advertiser registrations](#managing-advertiser-registrations).

## 7. Deliver the audience

Create a connection joining the dataset, the profile, and the advertiser:

```bash theme={null}
curl -X POST https://api.narrative.io/v2/connections \
  -H "Authorization: Bearer $NIO_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "connections_dataset",
    "dataset_id": 12345,
    "profile_id": "<profile_id>",
    "quick_settings": {
      "type": "first_party",
      "first_party_data_type": "first_party_external",
      "advertiser_ids": ["<their TTD advertiser id>"],
      "data_segment_names": ["In-Market Auto Q3"],
      "targeting_time_to_live_in_minutes": 129600,
      "historical_data_enabled": true
    }
  }'
```

The response is `201` with the connection:

```json theme={null}
{
  "type": "connections_dataset",
  "id": "<connection_id>",
  "app": { "id": 11, "name": "The TradeDesk Connector" },
  "dataset_id": 12345,
  "profile_id": "<profile_id>",
  "quick_settings": {
    "type": "first_party",
    "advertiser_ids": ["<their TTD advertiser id>"],
    "data_segment_names": ["In-Market Auto Q3"],
    "first_party_data_type": "first_party_external",
    "historical_data_enabled": true,
    "targeting_time_to_live_in_minutes": 129600
  },
  "status": "active",
  ...
}
```

The outer `type` identifies what you are connecting, and `quick_settings.type` selects the delivery interface.

`advertiser_ids` takes the advertiser's **Trade Desk** ID, the value you registered as `ttd_id`, not the `id` Narrative returns for the registration.

`data_segment_names` is the name the segment carries in the advertiser's seat. It is free text, not a taxonomy element.

[What the connection does](/reference/connectors/the-trade-desk.md#connection-settings) describes every field, its allowed values, and its default, along with what the connection does with existing and new rows. `historical_data_enabled` takes effect when the connection is created.

## 8. Confirm the connection

```bash theme={null}
curl -s "https://api.narrative.io/v2/connections?dataset_id=12345" \
  -H "Authorization: Bearer $NIO_API_TOKEN"
```

The response lists the `first_party` connection you created under `records`. `GET /v2/connections/{connection_id}` returns it on its own. Once delivery runs, the segment appears in the advertiser's seat under the name you gave it in `data_segment_names`, as though the advertiser had uploaded it themselves.

## Keeping the audience current

Write new data to the dataset. The connection keeps running, so new rows reach the advertiser's seat without further calls.

Refresh the dataset more often than the membership duration. Members that no delivery renews expire after `targeting_time_to_live_in_minutes`, and your source dataset must refresh at least every 90 days.

`advertiser_ids` takes a list, so one connection can deliver the audience to several registered advertisers.

## Managing advertiser registrations

List your registrations, or read one by its `id`:

```bash theme={null}
curl -s https://thetradedesk.narrativeconnectors.com/advertisers \
  -H "Authorization: Bearer $NIO_API_TOKEN"
# → {"records":[{"id":"<registration_id>","name":"Acme Motors","ttd_id":"<their TTD advertiser id>",...}]}

curl -s https://thetradedesk.narrativeconnectors.com/advertisers/{registration_id} \
  -H "Authorization: Bearer $NIO_API_TOKEN"
```

Replace a registration with `PUT`, for example when the advertiser gives you a new secret key. `PUT` replaces the whole record, so send all three fields:

```bash theme={null}
curl -X PUT https://thetradedesk.narrativeconnectors.com/advertisers/{registration_id} \
  -H "Authorization: Bearer $NIO_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme Motors",
    "ttd_id": "<their TTD advertiser id>",
    "secret": "<their new secret key>"
  }'
# → HTTP 200, the updated registration
```

Remove a registration you no longer deliver to. Delete the connections that deliver to that advertiser first:

```bash theme={null}
curl -X POST https://thetradedesk.narrativeconnectors.com/advertisers/{registration_id}/archive \
  -H "Authorization: Bearer $NIO_API_TOKEN"
# → HTTP 200
```

## Stopping a delivery

Delete the connection to stop delivering the audience:

```bash theme={null}
curl -X DELETE https://api.narrative.io/v2/connections/{connection_id} \
  -H "Authorization: Bearer $NIO_API_TOKEN"
# → HTTP 200, {}
```

## Troubleshooting

### Dataset and upload problems

| Symptom | Cause | Fix |
| - | - | - |
| `403` on `/company-info/whoami` | The key was not created under **Settings → API Keys** | Create one there. Application credentials do not work with this flow |
| `400 Missing required field .write_mode` or `.schema.file_config` | Both are required on dataset creation | Add them |
| `400 file not found` on ingest | You sent the path you requested rather than the one Narrative returned | Use the `path` from the upload response |
| Record count stays at zero | The file did not match the schema | Check that the format matches `file_config.type` and that field names match your schema exactly |
| `400 Dataset: Invalid Schema TTD` from the validation endpoint | The schema has no recognized identifier, or the identifier has no `value` property | Add one of the identifier columns listed under Required columns, in the shape that table shows |
| The Trade Desk listed under `errors` | The schema has no recognized identifier in the expected shape | Read that entry's `details`, then create a new dataset with a corrected schema |

### Delivery problems

| Symptom | Cause | Fix |
| - | - | - |
| The segment never appears in the advertiser's seat | The ID in `advertiser_ids` is not a registered `ttd_id`, or the registered secret key is out of date | Check `GET /advertisers` for the `ttd_id`, and `PUT` the registration with the advertiser's current secret key |
| Members disappear from the segment | No delivery renewed them within the membership duration | Refresh the dataset more often than `targeting_time_to_live_in_minutes` |

## Getting help

Contact your Narrative relationship manager with your company ID, the dataset ID, the connection ID, and the failing request and response. For questions about the advertiser's seat or credentials, the advertiser's TTD account representative is the faster path.

***

## Related content

> **[Third-party delivery via the API](/guides/connectors/the-trade-desk/third-party-delivery-api.md)**
>
> List an audience in TTD's marketplace instead

> **[The Trade Desk Connector](/reference/connectors/the-trade-desk.md)**
>
> Full settings reference, identifiers, and status vocabularies

> **[Connector Interfaces](/concepts/data-activation/connector-interfaces.md)**
>
> Why a dataset connects to an interface rather than a connector

> **[API Keys](/account-settings/api-keys.md)**
>
> Create and rotate keys for programmatic access


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