> ## 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.

# Sending Conversion Events to Pinterest via the API

> Upload conversion events to Narrative and send them to the Pinterest Conversions API over the API, first in test mode and then live

This guide shows you how to build an unattended integration that uploads conversion events to Narrative and sends them to the Pinterest Conversions API. Each event names the ad account it belongs to, so one connection serves every ad account the profile can reach. You start the connection in test mode to check your events, then switch to live delivery.

Before you start, complete the [prerequisites](/guides/connector-apis/pinterest#prerequisites) and [set up API access](/guides/connector-apis/pinterest#set-up-api-access). You need a profile ID and the IDs of the ad accounts your events belong to, from [List your ad accounts](/guides/connector-apis/pinterest#list-your-ad-accounts).

Every request in this guide goes to `https://api.narrative.io` with your API key.

## 1. Create the dataset

The dataset needs a `pinterest_conversion_event` column and at least one user identifier column. [Required columns](#required-columns) lists both.

```bash theme={null}
curl -X POST https://api.narrative.io/datasets \
  -H "Authorization: Bearer $NIO_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "store_checkouts_pinterest",
    "display_name": "Store Checkouts (Pinterest)",
    "description": "Offline checkouts for the Pinterest Conversions API",
    "write_mode": "append",
    "schema": {
      "file_config": { "type": "json", "normalize_field_names": false },
      "type": "object",
      "properties": {
        "pinterest_conversion_event": {
          "type": "object",
          "properties": {
            "pinterest_ad_account_id": { "type": "string" },
            "action_source": { "type": "string" },
            "event_id": { "type": "string" },
            "event_name": { "type": "string" },
            "event_time": { "type": "timestamptz" },
            "custom_data": {
              "type": "object",
              "properties": {
                "currency": { "type": "string" },
                "value": { "type": "double" },
                "order_id": { "type": "string" }
              }
            }
          }
        },
        "sha256_hashed_email": {
          "type": "object",
          "properties": { "value": { "type": "string" } }
        }
      }
    }
  }'
# → {"id": 12346, "company_id": 1234, "write_mode": "append", ...}
```

Record the `id`, then activate the dataset:

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

The response is `201` with the dataset, now `"status": "active"`.

### Required columns

The `pinterest_conversion_event` column is an object. These properties are required in every event:

| Property | Type | Meaning |
| - | - | - |
| `pinterest_ad_account_id` | `string` | The ad account that receives the event |
| `action_source` | `string` | Where the conversion happened. One of the [action sources](/reference/connectors/pinterest-conversions-api#action-sources), such as `offline` |
| `event_id` | `string` | Your unique ID for the event |
| `event_name` | `string` | One of the [event names](/reference/connectors/pinterest-conversions-api#event-names), such as `checkout` |
| `event_time` | `timestamptz` | When the event happened |

The optional properties are `event_source_url`, `partner_name`, `language` (all `string`), `opt_out` (`boolean`), `custom_data`, `device_info`, and `app_info`. [The `pinterest_conversion_event` attribute](/reference/connectors/pinterest-conversions-api#the-pinterest_conversion_event-attribute) gives the fields inside each object. If your schema includes `device_info`, declare `form_factor`, `os_family`, and `network_type` inside `device_info`.

The dataset also needs at least one of these identifier columns:

| Column | Shape of each value |
| - | - |
| `sha256_hashed_email` | `{"value": ...}` |
| `hashed_email` | `{"value": ..., "type": "..."}` |
| `sha256_hashed_phone_number` | `{"value": ...}` |
| `e164_phone_number` | A string |
| `telephone_number` | A string |
| `android_advertising_id` | `{"value": ...}` |
| `apple_idfa` | `{"value": ...}` |
| `mobile_id_unique_identifier` | `{"value": ..., "type": "..."}` |
| `unique_id` | `{"value": ..., "type": "..."}` |
| `narrative_id` | `{"context": "...", "value": ...}` |
| `ip_address` and `user_agent`, together | A string each |

Pinterest drops an event unless its row carries a hashed email, a mobile advertising ID, or both `ip_address` and `user_agent`. A `narrative_id` counts when it resolves to an email or a mobile ID. A phone number or `unique_id` helps Pinterest match the event but doesn't count on its own.

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 the required properties and one identifier looks like this:

```json theme={null}
{"pinterest_conversion_event": {"pinterest_ad_account_id": "<AD_ACCOUNT_ID>", "action_source": "offline", "event_id": "order-1001", "event_name": "checkout", "event_time": "2026-10-03T14:41:14Z"}, "sha256_hashed_email": {"value": "<SHA-256 of the trimmed, lowercased email>"}}
```

## 2. Confirm Pinterest 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 Pinterest, add `?tags=pinterest` and look for `conversion_events` with `"app_id": 20` in `accepted`:

```bash theme={null}
curl -s "https://api.narrative.io/datasets/{dataset_id}/interfaces?tags=pinterest" \
  -H "Authorization: Bearer $NIO_API_TOKEN"
# → {"dataset_id": 12346,
#    "accepted": [{"app_id": 20, "interface_id": "conversion_events"}, ...],
#    "errors": []}
```

## 3. Create the connection in test mode

Test mode sends each event to Pinterest with the `test` flag set, so you can check your events before you send live conversions.

```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": 12346,
    "profile_id": "<PROFILE_ID>",
    "quick_settings": {
      "type": "conversion_events",
      "historical_data_enabled": true,
      "test_mode_enabled": true
    }
  }'
# → 201 {"type": "connections_dataset", "id": "<CONNECTION_ID>",
#        "app": {"id": 20, "name": "Pinterest Connector"}, "dataset_id": 12346,
#        "installation_id": <INSTALLATION_ID>,
#        "quick_settings": {"type": "conversion_events", "test_mode_enabled": true,
#                           "historical_data_enabled": true},
#        "profile_id": "<PROFILE_ID>", "profile": {"id": "<PROFILE_ID>", "name": "Example Brand"},
#        "status": "active"}
```

[What the connection does](/guides/connector-apis/pinterest#what-the-connection-does) describes `historical_data_enabled`, `test_mode_enabled`, and their defaults.

Record the connection `id`.

## 4. Upload events

Each line of the file is one event, in a row that matches the schema you declared in [step 1](#1-create-the-dataset). Give every event its own `event_id`. For the columns Pinterest accepts and their shapes, see [The `pinterest_conversion_event` attribute](/reference/connectors/pinterest-conversions-api#the-pinterest_conversion_event-attribute), [Supported user identifiers](/reference/connectors/pinterest-conversions-api#supported-user-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.
</Warning>

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) to create the bucket, grant your AWS account access, and lay out the folders.

## 5. Go live

Create a live connection when the test connection's deliveries report `conversion.delivery.completed` (see [Delivery notifications](#delivery-notifications)). In the event's `data`, `stats.events` counts the events Pinterest processed, and `stats.counters` counts the events sent with each identifier (`email`, `phone`, `maid`, `external_id`, and `click_id`). `stats.rejections` counts the events the connector dropped, by reason: `event_too_old`, `missing_required_field`, `invalid_ad_account_id`, `missing_matching_identifier`, and `ad_account_not_in_profile`.

To create the live connection, send the same request as in [step 3](#3-create-the-connection-in-test-mode), but set `test_mode_enabled` to `false`:

```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": 12346,
    "profile_id": "<PROFILE_ID>",
    "quick_settings": {
      "type": "conversion_events",
      "historical_data_enabled": true,
      "test_mode_enabled": false
    }
  }'
# → 201 {"type": "connections_dataset", "id": "<CONNECTION_ID>",
#        "quick_settings": {"type": "conversion_events", "test_mode_enabled": false,
#                           "historical_data_enabled": true},
#        "status": "active", ...}
```

Then delete the test connection, using its `id` from step 3:

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

To see what is connected to the dataset:

```bash theme={null}
curl -s "https://api.narrative.io/v2/connections?dataset_id={dataset_id}" \
  -H "Authorization: Bearer $NIO_API_TOKEN"
# → {"records": [{"type": "connections_dataset", "id": "<CONNECTION_ID>",
#    "quick_settings": {"type": "conversion_events", ...}, "status": "active", ...}]}
```

## Delivery notifications

Subscribe a webhook as described in [Delivery notifications](/guides/connector-apis/pinterest#delivery-notifications). The connector sends `conversion.delivery.completed` when a delivery of conversion events finishes and `conversion.delivery.failed` when one fails.

## Stopping delivery

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

Deleting the connection stops sending events. To remove the dataset as well, send `DELETE /datasets/{dataset_id}`.

## Getting help

Contact your Narrative relationship manager with your company ID, the dataset ID, and the connection ID.

***

## Related content

<CardGroup cols={2}>
  <Card title="Pinterest Conversions API Connector" icon="pinterest" href="/reference/connectors/pinterest-conversions-api">
    Event fields, event names, and user identifiers
  </Card>

  <Card title="Delivering audiences via the API" icon="users" href="/guides/connector-apis/pinterest/audiences-api">
    Deliver hashed emails to a Pinterest audience
  </Card>

  <Card title="Hashing PII" icon="hashtag" href="/guides/ingestion/hashing-pii">
    Prepare identifiers for delivery
  </Card>

  <Card title="API Keys" icon="key" href="/account-settings/api-keys">
    Create and rotate keys for programmatic access
  </Card>
</CardGroup>


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