# Getting Started

## Getting your API Key

You can get an API key from [the user settings page in your Ad Reform account.](https://app.adreform.com/users/api_keys)

{% hint style="info" %}
Anyone with this API Key can use the Ad Reform API as if they were you. If it is compromised, you can create a new key and delete the old one from [your settings page](https://app.adreform.com/users/api_keys).
{% endhint %}

## Sending your first API call

You must set the following request headers when making API calls:

```shell
Content-Type: "application/json"
Authorization: "Bearer YOUR_API_KEY"
```

You will also need your organization’s `slug`, which will be passed along within request URLs.

You can find this in your browser’s URL bar when using the web UI:

![Finding your organization slug in the URL bar](https://archbee-image-uploads.s3.amazonaws.com/HNihl8dzgCp6O5s813cZx/0HsJeswDRqWLs_3CnSTVl_image.png)

## How it works

The Ad Reform API consists of two parts: a REST API to process requests, and outgoing webhooks to send out results.

For more on the API, go to the API Reference:

{% content-ref url="/pages/VglGOviSxtEMhPp87qtB" %}
[API Reference](/reference/api-reference)
{% endcontent-ref %}

For more on webhook events, go to the Webhooks Reference:

{% content-ref url="/pages/RuN7ElpscvurvgQtoCfJ" %}
[Webhooks Reference](/reference/webhooks-reference)
{% endcontent-ref %}

## Questions or feedback?

Let us know what questions or feeback you have! Just chat us or email <support@adreform.com>


# API Reference

## Screenshot Requests

{% content-ref url="/pages/i2oTGJcECJdEvHneJKXB" %}
[Create a Screenshot Request](/reference/api-reference/create-a-screenshot-request)
{% endcontent-ref %}

## Screenshots

{% content-ref url="/pages/LzU7fs0IHZS78gwuSzKC" %}
[List Screenshots](/reference/api-reference/list-screenshots)
{% endcontent-ref %}


# Create a Screenshot Request

{% openapi src="/files/QzHD2SJJhBbr3N2fe3uR" path="/api/v1/orgs/{organization\_slug}/screenshot\_requests" method="post" %}
[ad-reform-swagger.yaml](https://2718083151-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FWXKnh5nreVaQejzmqBUM%2Fuploads%2Fgit-blob-62087cc88de5585e2f431015233e37e65369350e%2Fad-reform-swagger.yaml?alt=media)
{% endopenapi %}

## Disabled organizations

When an organization is disabled (i.e. it does not have an active subscription), the API will return a 403 Forbidden response:

```json
{
  "status": "failure",
  "errors": {
    "organization": ["Your organization is disabled. Upgrade to keep using the API: https://adreform.com/o/_/billing"]
  }
}
```

## The Campaign object

The `campaign` object is used to tell us where to store the Ad and Screenshot data for the request. You must included either `campaign.id` *or* `campaign.name` in the request.

{% hint style="info" %}
Using `campaign.name` is the most common, as we will find *or* create a Campaign using this key
{% endhint %}

### `campaign.id`

The Ad Reform ID for an existing Campaign. Use this if you know the specific ID for the campaign you want to use, otherwise use `campaign.name`.

### `campaign.name`

The name you want to use for the Campaign. If a Campaign already exists with this name, we'll use that, otherwise we'll create a new one with this value as the name.

{% hint style="info" %}
Note: Campaign names cannot be longer than 250 characters. A `campaign.name` longer than 250 characters will be truncated automatically.
{% endhint %}

## The Ad object

The `ad` object (within each object in the `screenshot_requests` array) is used to indicate what ad data you want to use in the screenshots you're requesting. This can be entirely new ad data (i.e. an ad creative that does not yet exist in your Ad Reform account), or it can be the `id` or `lookup_key` pointing to an *existing* ad.

### `ad.id`

The Ad Reform ID for an existing Ad in your account.

### `ad.lookup_key`

A unique ID for an Ad. If an Ad already exists with this key, we'll use that Ad, otherwise we'll create a new one with this value as the `lookup_key`.

{% hint style="info" %}
Within a single API request, each `ad.lookup_key` must be unique across all entries in `screenshot_requests`. If you need multiple screenshots for the same ad, specify them within the `ad` object as multiple screenshots in the `screenshots` array.
{% endhint %}

### `ad.name`

An optional name for the ad. This is used when creating a new ad — either by providing `ad.media` alone, or by providing `ad.media` with a `lookup_key` that doesn't match any existing ad. When referencing an **existing** ad by `id` or `lookup_key`, this field is ignored and the existing ad's name is preserved.

{% hint style="info" %}
The `ad.name` field has a maximum length of 255 characters.
{% endhint %}

#### Example payload using `ad.lookup_key`

```json
{
  "screenshot_requests": [
    {
      "ad": {
        "lookup_key": "key-123",
        "media": { "type": "url", "content": "https://example.com/ad.png" }
      },
      "screenshots": [
        { "site": { "url": "https://website-one.com" }, "device": { "name": "desktop" } },
        { "site": { "url": "https://website-two.com" }, "device": { "name": "mobile" } }
      ]
    }
  ]
}
```

#### Example incorrect payload – duplicate `ad.lookup_key` across entries (rejected)

```json
{
  "screenshot_requests": [
    {
      "ad": {
        "lookup_key": "key-123",
        "media": { "type": "url", "content": "https://example.com/ad.png" }
      },
      "screenshots": [
        { "site": { "url": "https://website-one.com" }, "device": { "name": "desktop" } }
      ]
    },
    {
      "ad": {
        "lookup_key": "key-123",
        "media": { "type": "url", "content": "https://example.com/ad.png" }
      },
      "screenshots": [
        { "site": { "url": "https://website-two.com" }, "device": { "name": "mobile" } }
      ]
    }
  ]
}
```

### `ad.media`

An object with `type` and `content` keys that contains actual Ad creative data.

{% hint style="info" %}
You can use both `ad.media` (to send in raw ad data) and `ad.lookup_key` together in a request, so future requests with the same `lookup_key` will be associated to the same Ad in Ad Reform
{% endhint %}

#### **The Media object**

To create a new ad, provide ad creative content in the `media` object (within the `ad` object):

```json
"ad": {
  "media": {
    "type": "url"
    "content": "https://url-to-ad-creative-content.com"
  }
}
```

#### Supported media types

Here are the supported `type` values and some related `content` examples:

<table><thead><tr><th width="156.33333333333331">Type</th><th>Content</th><th>Description</th></tr></thead><tbody><tr><td>html_tag</td><td>&#x3C;img src="<a href="https://placehold.co/300x250">https://placehold.co/300x250</a>" width="300" height="250" /></td><td>An HTML string containing ad creative content (often a <code>&#x3C;script></code> tag or an <code>&#x3C;image></code> tag)</td></tr><tr><td>url</td><td><p>Image: <a href="https://placehold.co/300x250">https://placehold.co/300x250</a></p><p><br>Video: <a href="https://www.w3schools.com/tags/movie.mp4">https://www.w3schools.com/tags/movie.mp4</a></p></td><td>The URL for an ad creative, such as an image, video, VAST tag, or HTML5 zip file</td></tr></tbody></table>

## The Screenshots array

Within the `screenshots` array, there will be a JSON object for each screenshot for the associated ad. Each object is composed of `site` object and a `device` object.

```json
"screenshots": [
  {
    "device": {
        "name": "desktop"
    },
    "site": {
        "url": "https://www.adreform.com/"
    }
  }
]
```

### **The Site object**

The `Site` object in the `screenshots` array must include a `url` key.

#### `site.url`

The website URL.

```json
"site": {
  "url": "https://www.adreform.com"
}
```

### **The Device object**

The `Device` object in the `screenshots` array must include a `name` key.

#### `device.name`

The device to use when generating the screenshot.

```json
"device": {
  "name": "desktop"
}
```

#### Supported devices

Here's a list of supported `device.name` values:

* desktop
* mobile
* iphone
* tablet
* ipad
* Apple iPad
* Apple iPhone 5
* Apple iPhone 6
* Apple iPhone 6 Plus
* Apple iPhone X
* Apple iPhone 14 Pro Max
* Google Nexus 10
* Google Nexus 5
* Google Nexus 6
* Google Pixel 4
* Samsung Galaxy S10

### **The Options object**

You can use the `options` object within a `screenshot` object to specify additional settings.

#### Specifying which ad frames to use

You can use the `options.ad.frames` array to specify which frame(s) of the ad to use within a screenshot:

```json
"options": {
  "ad": {
    "frames": [
      { "marker": 0.5 },
      { "marker": 1.0 }
    ]
  }
}
```

Each object in the `frames` array should include a `marker`, which should be a `float` representing the timestamp (in seconds) where the frame should be captured. The `marker` value must be between 0 and 30 seconds.

{% hint style="info" %}
If `options.ad.frames` is an empty array (i.e. no frames are specified), we will use our default logic for capturing the ad.
{% endhint %}

#### Example payload using `options.ad.frames`

```json
"screenshots": [
  {
    "device": {
      "name": "desktop"
    },
    "site": {
      "url": "https://website-one.com"
    },
    "options": {
      "ad": {
        "frames": [
          { "marker": 0.5 },
          { "marker": 15.0 }
        ]
      }
    }
  }
]
```

{% hint style="info" %}
If a request payload includes multiple `screenshot` objects within a single `ad` object, and these `screenshot` objects specify `options.ad.frames`, we will merge all frame `marker`s into a single, de-duplicated set.
{% endhint %}

#### Example payload specifying frames with multiple `screenshot` objects

```json
{
  "screenshot_requests": [
    {
      "ad": {
        "media": {
          "type": "url",
          "content": "https://url-to-ad-video-content.com"
        }
      },
      "screenshots": [
        {
          "site": {
            "url": "https://website-one.com"
          },
          "device": {
            "name": "desktop"
          },
          "options": {
            "ad": {
              "frames": [
                { "marker": 0.0 },
                { "marker": 1.0 }
              ]
            }
          }
        },
        {
          "site": {
            "url": "https://website-two.com"
          },
          "device": {
            "name": "mobile"
          },
          "options": {
            "ad": {
              "frames": [
                { "marker": 1.0 },
                { "marker": 2.0 }
              ]
            }
          }
        }
      ]
    }
  ]
}
```

In this example:

* Since the `screenshot` objects are within the same `ad` object, we will merge the frame `marker` values
* We will generate 4 screenshots:
  * 2 on `website-one.com` at frames `0.0` and `1.0`
  * 2 on `website-two.com` at frames `1.0` and `2.0`


# Create an Ad

{% hint style="info" %}
This feature is currently in beta. Contact <support@adreform.com> if you're interested in trying the beta.
{% endhint %}

{% openapi src="/files/QzHD2SJJhBbr3N2fe3uR" path="/api/v1/orgs/{organization\_slug}/ads" method="post" %}
[ad-reform-swagger.yaml](https://2718083151-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FWXKnh5nreVaQejzmqBUM%2Fuploads%2Fgit-blob-62087cc88de5585e2f431015233e37e65369350e%2Fad-reform-swagger.yaml?alt=media)
{% endopenapi %}

## Overview

This endpoint allows you to create ads independently from the screenshot request workflow. This is useful when you want to:

* Pre-create ads before requesting screenshots
* Manage ad inventory separately from screenshot generation
* Build integrations that separate ad upload from screenshot workflows

## Idempotent Behavior

This endpoint is fully idempotent when using `lookup_key`:

* If an ad with the specified `lookup_key` already exists in the campaign, we'll return the existing ad with a `200 OK` status
* The `media` in the request is ignored when returning an existing ad
* New ads are created with a `201 Created` status

This makes it safe to retry requests without risking duplicate ad creation.

## Disabled organizations

When an organization is disabled (i.e. it does not have an active subscription), the API will return a 403 Forbidden response:

```json
{
  "status": "failure",
  "errors": {
    "organization": ["Your organization is disabled. Upgrade to keep using the API: https://adreform.com/o/_/billing"]
  }
}
```

## The Campaign object

The `campaign` object tells us where to store the ad. You must include either `campaign.id` *or* `campaign.name` in the request.

{% hint style="info" %}
Using `campaign.name` is the most common approach, as we will find *or* create a Campaign using this key.
{% endhint %}

### `campaign.id`

The Ad Reform ID for an existing Campaign. Use this if you know the specific ID for the campaign you want to use.

### `campaign.name`

The name you want to use for the Campaign. If a Campaign already exists with this name, we'll use that, otherwise we'll create a new one.

{% hint style="info" %}
Note: Campaign names cannot be longer than 250 characters. A `campaign.name` longer than 250 characters will be truncated automatically.
{% endhint %}

## The Ad object

The `ad` object contains the ad creative data.

### `ad.lookup_key` (optional)

A unique identifier for this ad. Use this to make requests idempotent - if an ad already exists with this `lookup_key` in the campaign, we'll return that ad instead of creating a new one.

### `ad.name` (optional)

A human-readable name for the ad.

### `ad.media` (required for new ads)

The ad creative content. Contains:

* `type`: Either `"url"` or `"html_tag"`
* `content`: The URL or HTML tag content

{% hint style="warning" %}
The `html_tag` content cannot exceed 1MB in size.
{% endhint %}

## Media Types

### URL (`type: "url"`)

Use this for ad tag URLs that resolve to a creative. The URL will be fetched and rendered to capture the ad preview.

```json
{
  "media": {
    "type": "url",
    "content": "https://example.com/ad-tag?size=300x250"
  }
}
```

### HTML Tag (`type: "html_tag"`)

Use this for raw HTML ad tags (like script tags or iframe embeds).

```json
{
  "media": {
    "type": "html_tag",
    "content": "<script src=\"https://example.com/ad.js\"></script>"
  }
}
```

## Subscribers (optional)

You can optionally subscribe to notifications when the ad preview is captured by including a `subscribers` array.

### Webhook Subscriber

Receive an `ad.uploaded` webhook event when the ad preview is ready:

```json
{
  "subscribers": [
    {
      "webhook": {
        "url": "https://your-endpoint.com/webhooks"
      }
    }
  ]
}
```

{% hint style="warning" %}
Webhook subscribers only receive notifications when a **new ad is created** (201 Created response). If an existing ad is returned via `lookup_key` (200 OK response), no webhook is sent. The existing ad data is returned in the API response, so you can use it directly.
{% endhint %}

### S3 Subscriber

Push the ad preview image to your S3 bucket when ready:

```json
{
  "subscribers": [
    {
      "s3": {
        "bucket": "your-bucket-name",
        "region": "us-east-1",
        "key_prefix": "ads/"
      }
    }
  ]
}
```

{% hint style="info" %}
S3 subscriptions require an S3 connector to be configured for your organization.
{% endhint %}

## Response

### Success Response (201 Created)

For newly created ads:

```json
{
  "type": "ad",
  "id": "uuid-of-created-ad",
  "name": "Ad Name",
  "lookup_key": "your-lookup-key",
  "status": "pending",
  "category": "display",
  "kind": "url",
  "campaign_id": "uuid-of-campaign",
  "created_at": "2026-01-17T10:30:00Z",
  "html_url": "https://app.adreform.com/o/your-org/campaigns/uuid/ads/uuid"
}
```

### Success Response (200 OK)

When an existing ad is returned via `lookup_key`, the response contains the ad's current state:

```json
{
  "type": "ad",
  "id": "uuid-of-existing-ad",
  "name": "Ad Name",
  "lookup_key": "your-lookup-key",
  "status": "uploaded",
  "category": "display",
  "kind": "url",
  "campaign_id": "uuid-of-campaign",
  "created_at": "2026-01-15T10:30:00Z",
  "html_url": "https://app.adreform.com/o/your-org/campaigns/uuid/ads/uuid"
}
```

### Status Values

| Status             | Description                             |
| ------------------ | --------------------------------------- |
| `pending`          | Ad created, preview capture in progress |
| `uploaded`         | Preview capture completed successfully  |
| `not_found`        | Media URL could not be found            |
| `net_read_timeout` | Network timeout during capture          |
| `generic_error`    | Other capture error                     |

### Error Response (422 Unprocessable Entity)

```json
{
  "status": "failure",
  "errors": {
    "media.content": ["exceeds maximum size of 1MB"]
  }
}
```

## Example Requests

### Create ad with URL media

```bash
curl -X POST "https://app.adreform.com/api/v1/orgs/your-org/ads" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "campaign": {
      "name": "Q1 2026 Campaign"
    },
    "ad": {
      "name": "Banner 300x250",
      "lookup_key": "banner-300x250-v1",
      "media": {
        "type": "url",
        "content": "https://example.com/ads/banner.png"
      }
    }
  }'
```

### Create ad with HTML tag and webhook subscriber

```bash
curl -X POST "https://app.adreform.com/api/v1/orgs/your-org/ads" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "campaign": {
      "name": "Q1 2026 Campaign"
    },
    "ad": {
      "name": "Rich Media Ad",
      "media": {
        "type": "html_tag",
        "content": "<script src=\"https://example.com/rich-media.js\"></script>"
      }
    },
    "subscribers": [
      {
        "webhook": {
          "url": "https://your-endpoint.com/ad-webhook"
        }
      }
    ]
  }'
```

### Idempotent request (returns existing ad)

```bash
# First request creates the ad (201 Created)
curl -X POST "https://app.adreform.com/api/v1/orgs/your-org/ads" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "campaign": {"name": "My Campaign"},
    "ad": {
      "lookup_key": "my-unique-key",
      "media": {"type": "url", "content": "https://example.com/ad.png"}
    }
  }'

# Retry request returns existing ad (200 OK)
curl -X POST "https://app.adreform.com/api/v1/orgs/your-org/ads" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "campaign": {"name": "My Campaign"},
    "ad": {
      "lookup_key": "my-unique-key",
      "media": {"type": "url", "content": "https://different-url.com"}
    }
  }'
# ^ Returns the existing ad, ignores the different media
```


# List Screenshots

{% hint style="info" %}
This feature is currently in beta. Contact <support@adreform.com> if you're interested in trying the beta.
{% endhint %}

{% openapi src="/files/QzHD2SJJhBbr3N2fe3uR" path="/api/v1/orgs/{organization\_slug}/screenshots" method="get" %}
[ad-reform-swagger.yaml](https://2718083151-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FWXKnh5nreVaQejzmqBUM%2Fuploads%2Fgit-blob-62087cc88de5585e2f431015233e37e65369350e%2Fad-reform-swagger.yaml?alt=media)
{% endopenapi %}

## Overview

This endpoint returns approved screenshots with their associated ad data, site information, and screenshot set details. Use it to poll for newly approved screenshots within a time range.

## Query Parameters

| Parameter  | Required | Description                                                                                                                           |
| ---------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `from`     | No       | Start of approval time range (ISO 8601 datetime). Defaults to 24 hours ago.                                                           |
| `to`       | No       | End of approval time range (ISO 8601 datetime). Defaults to current time.                                                             |
| `order`    | No       | Sort order by `approved_at`. Either `asc` (default) or `desc`.                                                                        |
| `page`     | No       | Page number for pagination                                                                                                            |
| `per_page` | No       | Number of results per page (default: 100, max: 500)                                                                                   |
| `include`  | No       | Comma-separated list of additional data to include. Supported: `ad.creative` (includes platform creative data and preview image URL). |

## Pagination

Results are paginated using `Link` response headers.

### Response Headers

| Header | Description                                         |
| ------ | --------------------------------------------------- |
| `Link` | Standard pagination links (`first`, `prev`, `next`) |

### Example

```http
Link: <https://app.adreform.com/api/v1/orgs/abc-company/screenshots?from=2026-01-01T00:00:00Z&page=2&per_page=100>; rel="next"
```

## Polling Pattern

Use this endpoint to poll for newly approved screenshots:

1. Store a `last_polled_at` timestamp
2. On each poll, set `from` to your `last_polled_at` value
3. Process all pages of results
4. Update `last_polled_at` to the current time

```bash
# Initial poll - get all screenshots approved in the last 24 hours
curl "https://app.adreform.com/api/v1/orgs/your-org/screenshots" \
  -H "Authorization: Bearer YOUR_API_KEY"

# Subsequent poll - get screenshots approved since last poll
curl "https://app.adreform.com/api/v1/orgs/your-org/screenshots?from=2026-03-01T12:00:00Z" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

## Response

### Success Response (200 OK)

```json
{
  "screenshots": [
    {
      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "status": "uploaded",
      "image_url": "https://app.adreform.com/attachments/perm/...",
      "device": "desktop",
      "site": {
        "url": "https://example.com",
        "host": "example.com"
      },
      "ad": {
        "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
        "name": "Banner 300x250",
        "lookup_key": "banner-300x250-v1",
        "external_id": null,
        "width": 300,
        "height": 250,
        "media": {
          "type": "html_tag"
        }
      },
      "screenshot_set": {
        "id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
        "created_at": "2026-03-01T10:00:00Z",
        "approved_at": "2026-03-01T14:30:00Z",
        "status": "approved",
        "campaign": {
          "id": "d4e5f6a7-b8c9-0123-def1-234567890123",
          "name": "Q1 2026 Campaign"
        }
      }
    }
  ]
}
```

### Platform Creative Data

Platform creative fields (`xandr_creative`, `google_dv360_creative`, `google_ad_manager_creative`, `trade_desk_creative`) and `preview_image_url` are only included when `?include=ad.creative` is passed. When included, the corresponding fields are populated if the ad is linked to a platform creative:

#### Xandr

```json
"xandr_creative": {
  "xandr_id": 123456,
  "name": "My Xandr Creative",
  "line_item": {
    "xandr_id": 789012,
    "name": "My Line Item",
    "state": "active"
  }
}
```

#### Google DV360

```json
"google_dv360_creative": {
  "google_dv360_id": "123456",
  "display_name": "My DV360 Creative",
  "line_item": {
    "google_dv360_id": "789012",
    "display_name": "My Line Item",
    "entity_status": "ENTITY_STATUS_ACTIVE"
  }
}
```

#### Google Ad Manager

```json
"google_ad_manager_creative": {
  "google_ad_manager_id": "123456",
  "name": "My GAM Creative",
  "line_item": {
    "google_ad_manager_id": "789012",
    "name": "My Line Item",
    "status": "DELIVERING"
  }
}
```

#### The Trade Desk

```json
"trade_desk_creative": {
  "trade_desk_id": "abc123",
  "name": "My TTD Creative",
  "ad_group": {
    "trade_desk_id": "def456",
    "name": "My Ad Group"
  }
}
```

## Disabled Organizations

When an organization is disabled (i.e. it does not have an active subscription), the API will return a 403 Forbidden response:

```json
{
  "status": "failure",
  "errors": {
    "organization": ["Your organization is disabled. Upgrade to keep using the API: https://adreform.com/o/_/billing"]
  }
}
```

## Error Responses

### 401 Unauthorized

Returned when the API key is missing or invalid.

### 403 Forbidden

Returned when the organization is disabled or the `api_screenshot_polling` feature is not enabled.

### 422 Unprocessable Entity

Returned when query parameters are invalid.

```json
{
  "status": "failure",
  "errors": {
    "from": ["is not a valid ISO 8601 datetime"]
  }
}
```


# Webhooks Reference

## How they work

Webhook URLs will receive a POST request with JSON data related to the event that triggered the webhook.

See [Events](#events) below for supported events and the associated JSON payloads.

## Error handling

There are things that can go wrong even before a request reaches its destination (e.g. DNS resolution error, sudden network failure, etc.) and even if it does, the target system may not always be able to correctly process the request. This is why the notification delivery system is built to handle these failures.

Our webhook delivery system uses response status to determine whether a request has been successful or not.

* 2XX response codes: The request is considered successful and the notification is considered as delivered, no retry needed.
* Any other response status or error (such as connection error, timeout, etc.) is handled as an unsuccessful delivery attempt and the task is rescheduled for later.

### Retries

If a webhook delivery fails, it gets retried. We use an exponential backoff strategy to determine how long to wait before attempting the delivery again.

We'll retry a webhook delivery up to 14 times (\~24 hours of retrying), after which the delivery will be marked as `retries exhausted` and retries will stop.

## Events

### `ad.uploaded`

This event is fired when an ad preview has been successfully captured. Use this to get notified when ads created via the Create Ad API are ready.

{% hint style="warning" %}
This event is only fired when a **new ad** is created. If you use a `lookup_key` and an existing ad is returned, no webhook is sent. Check the API response directly for the ad data.
{% endhint %}

{% hint style="info" %}
The `width` and `height` fields are optional and will only be present if the ad dimensions were successfully detected.
{% endhint %}

<details>

<summary>Payload</summary>

```json
{
  "id": "evt_{{uuid-of-event}}",
  "type": "event",
  "created_at": "2026-01-17T10:52:11.822-05:00",
  "topic": "ad.uploaded",
  "data": {
    "object": {
      "type": "ad",
      "id": "uuid-of-ad",
      "lookup_key": "your-lookup-key-or-null",
      "name": "Ad Name",
      "status": "uploaded",
      "category": "display",
      "kind": "html_tag",
      "width": 300,
      "height": 250,
      "preview_image_url": "https://app.adreform.com/path/to/preview.png",
      "html_url": "https://app.adreform.com/o/your-org/campaigns/uuid/ads/uuid",
      "campaign": {
        "id": "uuid-of-campaign",
        "name": "Campaign Name"
      }
    }
  }
}
```

</details>

***

### `screenshot_set.approved`

This event is fired when a set of screenshots is ready for download

<details>

<summary>Payload</summary>

```json
{
  "id": "evt_{{uuid-of-event}}", // unique ID for this event
  "type": "event",
  "created_at": "2023-01-01T10:52:11.822-05:00",
  "topic": "screenshot_set.approved",
  "data": {
    "object": {
      "type": "screenshot_set",
      "id": "uuid-of-screenshot-set",
      "campaign_id": "uuid-of-campaign", // ID of the Campaign in Ad Reform
      "created_at": "2023-01-01T10:51:23.961-05:00",
      "status": "approved", // this will always be "approved"
      "instructions": "",   // the special instructions sent in the original request
      "review_notes": "",   // notes from Ad Reform if there was a problem with the screenshot
      "screenshots": [
        {
          "type": "screenshot",
          "id": "uuid-of-screenshot", // ID of the Screenshot in Ad Reform
          "status": "uploaded",
          "image_url": "https://app.adreform.com/path/image.png",
          "device": "Desktop",
          "site": {
            "url": "https://theverge.com",
            "host": "theverge.com"
          },
          "ad": {
            "type": "ad",
            "id": "uuid-of-ad", // ID of the Ad in Ad Reform
            "status": "uploaded",
            "lookup_key": "ad-lookup-key-if-present", // or null
            "name": "name-if-present", // name of the Ad in Ad Reform
            "external_id": null,
            "preview_image_url": "https://app.adreform.com/path/image.png",
            "width": 300,
            "height": 250,
            "html_url": "https://app.adreform.com/path/to/ad/in-webui",
            "media": {
              "type": "image_file" // see Media object in docs for possible values
            },
            // if ad was synced from Xandr, this will be present
            "xandr_creative": {
              "xandr_id": 440600236,
              "name": "ad_reform_300x250",
              "line_item": {
                "xandr_id": 21352428,
                "name": "Example Display-EN_O6X6TSIFL-1261",
                "state": "active"
              }
            },
            // if ad was synced from Google Display & Video 360, this will be present
            "google_dv360_creative": {
              "google_dv360_id": "609196778",
              "display_name": "ad_reform_300x250",
              "line_item": {
                "google_dv360_id": "22170535644",
                "display_name": "My Line Item",
                "entity_status": "ENTITY_STATUS_ACTIVE"
              }
            },
            // if ad was synced from Google Ad Manager, this will be present
            "google_ad_manager_creative": {
              "google_ad_manager_id": 138479065953,
              "name": "ad_reform_300x250",
              "line_item": {
                "google_ad_manager_id": 6742858256,
                "name": "My Line Item",
                "status": "READY"
              }
            },
            // if ad was synced from The Trade Desk, this will be present
            "trade_desk_creative": {
              "trade_desk_id": "ocy4qu03",
              "name": "ad_reform_300x250",
              "ad_group": {
                "trade_desk_id": "3j6rtyu",
                "name": "My Ad Group"
              }
            }
          },
          // if a bucket and key_prefix were specified
          // as `subscribers` in the Screenshot Request
          // we'll include info about the S3 objects here
          "s3_objects": [
            {
              "bucket": "my-bucket-name",   // bucket containing the screenshot
              "key": "key-prefix/uuid.png", // object key for the screenshot
              "status": "uploaded"          // will be "upload_failed" if there was a problem uploading
            }
          ],
          // if the screenshot was requested using a specific frame
          // we'll include frame information here
          "frame": {
            "marker": 1.5,                                          // timestamp in seconds when the frame was captured
            "status": "uploaded",                                   // status of the frame capture
            "image_url": "https://app.adreform.com/path/frame.png"  // URL to the frame image
          }
        },
        {
          "type": "screenshot",
          "id": "uuid",
          "status": "uploaded",
          "image_url": "https://app.adreform.com/path/image2.png",
          "device": "Desktop",
          "site": {
            "url": "https://espn.com",
            "host": "espn.com"
          },
          "ad": {
            "id": "uuid-of-ad",
            "status": "uploaded",
            "lookup_key": "ad-lookup-key-if-present",
            "type": "ad",
            "name": "name-if-present",
            "external_id": null,
            "preview_image_url": "https://app.adreform.com/path/image.png",
            "width": 300,
            "height": 250,
            "html_url": "https://app.adreform.com/path/to/ad/in-webui",
            "media": {
              "type": "image_file"
            },
            "xandr_creative": null,
            "google_dv360_creative": null,
            "google_ad_manager_creative": null,
            "trade_desk_creative": null
          },
          "s3_objects": [
            {
              "bucket": "my-bucket-name",
              "key": "key-prefix/uuid.png",
              "status": "uploaded"
            }
          ],
          "frame": null  // will be null if screenshot is not associated with a frame
        }
      ]
    }
  }
}
```

</details>

## Objects

### `ad`

This object contains data about the `ad` creative. Notable fields include:

#### `status`

| Status             | Description                                             |
| ------------------ | ------------------------------------------------------- |
| uploaded           | The ad creative was captured successfully               |
| not\_found         | We couldn't find the creative asset when loading the ad |
| net\_read\_timeout | We encountered network issues when loading the ad       |
| generic\_error     | We encountered an error when loading the ad             |

***

### `screenshot`

This object contains data about the `screenshot`. Notable fields include:

#### `status`

| Status             | Description                                                                |
| ------------------ | -------------------------------------------------------------------------- |
| uploaded           | The screenshot was generated successfully                                  |
| ad\_not\_captured  | The associated `ad` was not captured so the screenshot cannot be generated |
| net\_read\_timeout | We encountered network issues when generating the screenshot               |
| no\_slots\_found   | The requested site had no ad slots matching the associated `ad` dimentions |
| site\_unreachable  | The requested site was not reachable                                       |
| ssl\_error         | We encountered an SSL error when generating the screenshot                 |
| generic\_error     | We encountered an error when capturing the screenshot                      |

#### `frame`

This object contains data about which `frame` was specified for the screenshot. Notable fields include:

**`status`**

| Status   | Description                         |
| -------- | ----------------------------------- |
| uploaded | The frame was captured successfully |
| failed   | The frame capture failed            |
| pending  | The frame is waiting to be captured |

The `frame` field will be:

* Present with frame information when the screenshot is associated with a specific frame
* `null` when the screenshot is not associated with a frame


