# Reddit Ads

Run image, video, carousel and text ads on Reddit, promote existing posts, target subreddits and keywords, and send conversions to a Reddit pixel.

Reddit Ads works through its own connection, `redditads`, with its own authorization and its own `accountId`. It does not depend on an organic Reddit account, which matters because organic publishing to Reddit is paused (see [Reddit](https://pozy.ai/docs/platforms/reddit)). Campaigns, targeting, reporting and the Conversions API all work.

| | |
| --- | --- |
| Connection platform | `redditads` |
| `platform` on ad endpoints | `reddit` |
| Structure | Campaign, ad group, ad, all created by one call |
| Goals | `traffic`, `awareness`, `conversions`, `video_views`, `lead_generation` |
| Formats | Image, video, text, a carousel of 2 to 10 cards, or a post that already exists |
| Headline | Required, up to 300 characters |
| Changes after launch | Status, name, budget, bid and targeting |
| Audiences | Target audiences built in Reddit Ads Manager, by id |
| Measurement | Reddit pixel (created in Reddit) and the Conversions API |

## Before you start

Connect with a Reddit login that has a role on one or more ad accounts in Reddit Ads Manager. Set up two things there first. Reddit insists on both, and its API offers no way to make them:

- **A pixel.** Each ad group carries one, even when the goal has nothing to do with conversions. If the ad account has none, creating an ad fails with `422 feature_not_available`. Add one in Events Manager inside Reddit Ads Manager.
- **A profile.** On Reddit, every ad is really a post by a profile that belongs to the ad account. Pozy uses the account's first profile for new ads.

## Connect

Call the connect endpoint with the profile. There is no posting account involved, so there is no `accountId` on this call.

```bash cURL
curl "https://api.pozy.ai/v1/connect/redditads/ads?profileId=66b2e19d8c3f5a7e9d0b1c2a&redirect_url=https://app.example.com/ads/connected" \
  -H "Authorization: Bearer $POZY_API_KEY"
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/connect/redditads/ads?profileId=66b2e19d8c3f5a7e9d0b1c2a&redirect_url=https:%2F%2Fapp.example.com%2Fads%2Fconnected", {
  headers: { Authorization: `Bearer ${process.env.POZY_API_KEY}` },
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
console.log(data.authUrl ?? data.accountId);
```
```python Python
import os, requests

resp = requests.get(
    "https://api.pozy.ai/v1/connect/redditads/ads",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    params={"profileId": "66b2e19d8c3f5a7e9d0b1c2a", "redirect_url": "https://app.example.com/ads/connected"},
)
print(resp.json())
```

The response carries an `authUrl`. Reddit's consent screen asks for three permissions:

| Permission | Used for | Required |
| --- | --- | --- |
| `adsread` | Reading ad accounts, campaigns, ads, pixels and metrics | Yes |
| `adsedit` | Creating and changing campaigns, ad groups, ads and their posts | Yes |
| `adsconversions` | Sending events through the Conversions API | No, but conversions fail without it |

After consent the browser lands on your `redirect_url`. Call the connect endpoint again to read the connection:

```json
{
  "alreadyConnected": true,
  "accountId": "66b2e19d8c3f5a7e9d0b1c2d",
  "platform": "redditads",
  "username": "acme_marketing",
  "displayName": "Reddit Ads (acme_marketing)"
}
```

On failure the redirect carries `platform=redditads` and an `error`: `reddit_ads_auth_failed` (declined), `reddit_ads_scope_not_granted`, `reddit_ads_no_ad_accounts` or `reddit_ads_callback_error`.

### Choose ad accounts

By default Pozy syncs all the ad accounts the Reddit login has access to. To limit that, send `adAccountId` on the connect call, or `adAccountIds` as a repeated or comma-separated list. Ids have the form `a2_gu5tcd9ah5se`; anything else is rejected with `400`.

### Reconnect without losing history

When the token stops working, or the connection lacks one of the three permissions, the connect call returns an `authUrl` again instead of `alreadyConnected`. Sending the person through it reconnects the same `accountId`, so synced ads and stored ids stay as they are. Pass `force=true` to get a fresh `authUrl` even when the connection is healthy.

> **Reconnect, do not disconnect:**
> 
> Disconnecting and connecting again creates a new connection with a new `accountId`. To fix a permission, run the connect call on the existing one.

## Create an ad

`POST /v1/ads/create` writes the post and builds the campaign, ad group and ad. The creative fields you send pick the format.

```bash cURL
curl -X POST https://api.pozy.ai/v1/ads/create \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: planner-launch-reddit" \
  -d '{
    "accountId": "66b2e19d8c3f5a7e9d0b1c2d",
    "adAccountId": "a2_gu5tcd9ah5se",
    "name": "Planner launch",
    "goal": "traffic",
    "budgetAmount": 50,
    "budgetType": "daily",
    "headline": "The planner your team will actually open",
    "body": "Tasks, docs and meetings in one place. Free for teams of 5.",
    "imageUrl": "https://cdn.example.com/planner.png",
    "linkUrl": "https://example.com/planner",
    "callToAction": "SIGN_UP",
    "countries": ["US", "CA"],
    "targeting": {
      "communities": ["productivity", "r/projectmanagement"],
      "conversationKeywords": ["task manager"]
    }
  }'
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/ads/create", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.POZY_API_KEY}`,
    "Idempotency-Key": "planner-launch-reddit",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    accountId: "66b2e19d8c3f5a7e9d0b1c2d",
    adAccountId: "a2_gu5tcd9ah5se",
    name: "Planner launch",
    goal: "traffic",
    budgetAmount: 50,
    budgetType: "daily",
    headline: "The planner your team will actually open",
    body: "Tasks, docs and meetings in one place. Free for teams of 5.",
    imageUrl: "https://cdn.example.com/planner.png",
    linkUrl: "https://example.com/planner",
    callToAction: "SIGN_UP",
    countries: ["US", "CA"],
    targeting: {
      communities: ["productivity", "r/projectmanagement"],
      conversationKeywords: ["task manager"],
    },
  }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
console.log(data.ad._id, data.ad.status);
```
```python Python
resp = requests.post(
    "https://api.pozy.ai/v1/ads/create",
    headers={
        "Authorization": f"Bearer {os.environ['POZY_API_KEY']}",
        "Idempotency-Key": "planner-launch-reddit",
    },
    json={
        "accountId": "66b2e19d8c3f5a7e9d0b1c2d",
        "adAccountId": "a2_gu5tcd9ah5se",
        "name": "Planner launch",
        "goal": "traffic",
        "budgetAmount": 50,
        "budgetType": "daily",
        "headline": "The planner your team will actually open",
        "body": "Tasks, docs and meetings in one place. Free for teams of 5.",
        "imageUrl": "https://cdn.example.com/planner.png",
        "linkUrl": "https://example.com/planner",
        "callToAction": "SIGN_UP",
        "countries": ["US", "CA"],
        "targeting": {
            "communities": ["productivity", "r/projectmanagement"],
            "conversationKeywords": ["task manager"],
        },
    },
)
print(resp.json()["ad"]["status"])
```

```json
{
  "ad": {
    "_id": "66f0a1b2c3d4e5f6a7b8c9d0",
    "name": "Planner launch",
    "platform": "reddit",
    "status": "pending_review",
    "adType": "standalone",
    "goal": "traffic",
    "budget": { "amount": 50, "type": "daily" },
    "platformCampaignId": "2160485120262584829",
    "platformAdSetId": "2160485120597863293",
    "platformAdId": "2160485120873925373",
    "platformAdAccountId": "a2_gu5tcd9ah5se"
  },
  "message": "Ad created and submitted for review"
}
```

The campaign and ad group are named `<name> - Campaign` and `<name> - Ad Group` unless you send `campaignName` and `adSetName`. If Reddit rejects a later step, Pozy archives what it already created so a failed request does not leave an empty campaign behind.

Send `status: "PAUSED"` to build the whole campaign switched off and check it before any money goes out. Set `campaignStatus` when the campaign should differ from its ad group and ad.

### Formats

| Format | Send |
| --- | --- |
| Image | `headline`, `imageUrl`, `linkUrl`, optional `body` and `callToAction` |
| Video | `headline`, `video.url`, a thumbnail in `video.thumbnailUrl` or `imageUrl`, `linkUrl`, and optionally `body` and `callToAction`. The video must be a URL: `video.id` is refused. |
| Carousel | `headline`, between 2 and 10 `carouselCards`, and optionally `body`. A card has `imageUrl`, `callToAction`, a caption (its `headline` or `description`) and a `linkUrl`, which defaults to the ad's own. |
| Text | Just `headline` and `body`. Text ads have no destination, so sending `linkUrl` gets `400`. |

Behind the scenes Reddit turns the creative into a post by the ad account's profile, open to comments, and the ad promotes it.

Button labels for `callToAction`: `LEARN_MORE`, `SHOP_NOW`, `SIGN_UP`, `CONTACT_US`, `DOWNLOAD`, `GET_QUOTE`, `SUBSCRIBE`, `APPLY_NOW`, `BOOK_NOW`, `BUY_TICKETS`, `DONATE_NOW`, `GET_DIRECTIONS`, `GET_SHOWTIMES`, `LISTEN_NOW`, `ORDER_NOW`, `SEE_MENU` or `INSTALL_MOBILE_APP`. Omit the field to show no button.

### Goals and the pixel

| `goal` | Optimizes for | Needs |
| --- | --- | --- |
| `traffic` | Clicks | |
| `awareness` | Impressions | |
| `video_views` | 6-second views (`optimizationGoal: "VIDEO_VIEW_6S"`, the default) or 15-second views (`VIDEO_VIEW_15S`) | A video creative |
| `conversions` | One pixel event, set in `promotedObject.customEventType` | A pixel that has already received that event |
| `lead_generation` | The pixel's `LEAD` event, sending people to `linkUrl` | A pixel that has already received `LEAD` |

Every ad group carries a pixel. Pozy uses `promotedObject.pixelId` (or `tracking.pixelId`) when you send one, and the ad account's first pixel otherwise. List pixels with `GET /v1/accounts/{accountId}/conversion-destinations`.

On `conversions`, `customEventType` is one of `PURCHASE`, `LEAD`, `SIGN_UP` (or `COMPLETE_REGISTRATION`), `ADD_TO_CART`, or `PAGE_VISIT` (or `VIEW_CONTENT`, `CONTENT_VIEW`):

```json
{
  "goal": "conversions",
  "promotedObject": { "pixelId": "a2_gu5tcd9ah5se", "customEventType": "PURCHASE" }
}
```

> **The event has to exist first:**
> 
> Reddit only optimizes toward an event the pixel has already seen. Before a `conversions` or `lead_generation` create, Pozy checks when the pixel last received it. If it never did, the call returns `422 invalid_resource_state`. Fire the event from your site, or send one through the Conversions API, then retry.

Both goals use Reddit's 7-day click, 1-day view attribution window. Reddit retired its native lead forms on September 30, 2026, so `lead_generation` drives traffic to your own page and `leadGenFormId` returns `400`.

### Budget, schedule and bidding

- The budget sits on the ad group. `daily` spends up to `budgetAmount` per day; `lifetime` spends it over the flight and needs `endDate`.
- `startDate` defaults to now. A date or time without an offset is read in the ad account's time zone, and a date-only `endDate` runs to 23:59:59 that day.
- New ad groups bid automatically. Set a manual bid or a cost cap after creation (see below).

## Promote an existing post

`POST /v1/ads/boost` promotes a Reddit post that already exists, with its votes and comments. Pass the Reddit post id as `platformPostId`, with or without the `t3_` prefix. Targeting goes inside the `targeting` object.

```bash
curl -X POST https://api.pozy.ai/v1/ads/boost \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: boost-t3-1o4x9qz" \
  -d '{
    "accountId": "66b2e19d8c3f5a7e9d0b1c2d",
    "adAccountId": "a2_gu5tcd9ah5se",
    "platformPostId": "t3_1o4x9qz",
    "name": "Boost launch thread",
    "goal": "traffic",
    "budget": { "amount": 30, "type": "daily" },
    "linkUrl": "https://example.com/planner",
    "targeting": { "countries": ["US"], "communities": ["productivity"] }
  }'
```

The post you boost must come from a profile that belongs to the ad account. Boosting someone else's post fails with `422` and `param: "platformPostId"`; add that author as a profile of the ad account in Reddit Ads Manager, then try again. The `video_views` goal only works on a video post.

## Targeting

| Field | Values |
| --- | --- |
| `countries` | ISO codes. With no location at all, the ad targets the US. |
| `regions`, `metros`, `cities`, `zips` | `{ "key": "..." }` from targeting search, `dimension=geo` |
| `excludedLocations` | `countries`, `regions`, `cities` and `zips` only |
| `gender` | `all`, `male` or `female` |
| `languages` | Codes such as `en` or `de` |
| `targeting.interests` | `{ "id": "..." }` from targeting search, `dimension=interest` |
| `targeting.communities`, `targeting.excludedCommunities` | Subreddit names, with or without `r/` |
| `targeting.conversationKeywords`, `targeting.excludedConversationKeywords` | Free text |
| `audienceInclude`, `audienceExclude` | Ids of custom audiences built in Reddit Ads Manager |

Age, income, behaviors and radius targeting return `400` naming the field. Custom audiences cannot be created or uploaded through the API (`POST /v1/ads/audiences` returns `422` on a `redditads` connection), so build them in Reddit and target them by id.

Search Reddit's catalog with `GET /v1/ads/targeting/search` and `dimension` set to `geo`, `interest`, `community` or `conversationKeyword`:

```bash
curl "https://api.pozy.ai/v1/ads/targeting/search?accountId=66b2e19d8c3f5a7e9d0b1c2d&q=gaming&dimension=community" \
  -H "Authorization: Bearer $POZY_API_KEY"
```

A community result's `id` is the subreddit name to send, with its subscriber count in `audienceSize`. On `geo`, `geoType` is `city` (default), `zip`, `region`, `metro_area`, `country` or `all`.

## Manage a running campaign

Campaigns, ad groups and ads each have their own status on Reddit. Pozy changes only the one you address.

| Change | Call | Notes |
| --- | --- | --- |
| Pause or resume | `PUT /v1/ads/{adId}/status`, `/v1/ads/ad-sets/{adSetId}/status` or `/v1/ads/campaigns/{campaignId}/status` | Only that level's switch changes. |
| Rename | `name` on `PUT /v1/ads/{adId}`, `/v1/ads/ad-sets/{adSetId}` or `/v1/ads/campaigns/{campaignId}` | |
| Budget | `budget: { amount, type }` on `PUT /v1/ads/{adId}` or `PUT /v1/ads/ad-sets/{adSetId}` | A daily budget stays daily and a lifetime one stays lifetime; trying to switch gets `422`. |
| Bid | `bidStrategy` and `bidAmount` on `PUT /v1/ads/ad-sets/{adSetId}` | Strategies: `COST_CAP`, `LOWEST_COST_WITH_BID_CAP`, `LOWEST_COST_WITHOUT_CAP` |
| Targeting | `targeting` on `PUT /v1/ads/{adId}` | Replaces the whole set. A field you leave out is cleared (locations fall back to the US). |
| Stop | `DELETE` on the ad, ad group or campaign | Archives on Reddit and marks Pozy's copy `cancelled`. |

`GET /v1/ads/ad-sets/{adSetId}/targeting` reads an ad group's current targeting and schedule before you replace it. Creative edits are not available (`501 feature_not_available`); create a new ad instead.

`POST /v1/ads/campaigns` creates a campaign on its own. With a `budget` it becomes a campaign-budget campaign, where the budget changes through `PUT /v1/ads/campaigns/{campaignId}` rather than the ad group. Adding an ad to an existing campaign or ad group (`existingCampaignId`, `adSetId`) is not available on Reddit.

Pozy also imports the ads you build directly in Reddit Ads Manager: `GET /v1/ads?platform=reddit` shows all ads of the synced ad accounts, wherever they were made. Once Reddit drops an archived object from its lists, Pozy marks it `cancelled`.

## Measure results

For each ad and each day (in the ad account's time zone), Pozy stores impressions, clicks and spend, and works out CTR, CPC and CPM from them. `GET /v1/ads/{adId}/analytics` returns one ad; `GET /v1/ads/tree` sums them up the campaign tree.

- Those three metrics are the whole sync. `reach`, video and conversion metrics read `0` on Reddit.
- On connect, Pozy imports the last 90 days.
- Demographic `breakdowns` are not available.

### Conversions API

Pixels are created in Reddit Ads Manager; the tracking tags endpoints do not manage Reddit pixels. Server-side events go to `POST /v1/ads/conversions` with the pixel id as `destinationId`:

```bash
curl -X POST https://api.pozy.ai/v1/ads/conversions \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "accountId": "66b2e19d8c3f5a7e9d0b1c2d",
    "destinationId": "a2_gu5tcd9ah5se",
    "events": [{
      "eventName": "Purchase",
      "eventTime": 1798804800,
      "eventId": "order_12345",
      "value": 25.99,
      "currency": "USD",
      "user": { "email": "customer@example.com", "clickIds": { "rdt_cid": "1234567890abcdef" } }
    }]
  }'
```

- Each event needs a match key: `clickIds.rdt_cid`, `clickIds.rdt_uuid`, an email, a phone, an `externalId`, or the IP address with the user agent.
- `eventTime` is Unix seconds. Reddit accepts events up to 7 days old.
- `eventId` is Reddit's dedupe key against the same event fired by the pixel.
- The connection needs `adsconversions`. Without it the call returns `403 reconnect_required`.

[Conversions](https://pozy.ai/docs/ads/conversions) covers event names, hashing and batching across networks.

## Rate limits

Reddit limits requests per connected Reddit user. When Reddit answers `429` with a short wait, Pozy waits and retries up to twice for you. A longer wait comes back as `429 rate_limited` with a `Retry-After` header. Your workspace's own [rate limit](https://pozy.ai/docs/getting-started/rate-limits) applies on top.

## Errors you will see

| Status and code | Cause | Fix |
| --- | --- | --- |
| `422 feature_not_available` ("has no pixel" or "has no profile") | The ad account is missing a pixel or a profile | Create it in Reddit Ads Manager, then retry. |
| `422 invalid_resource_state` | The campaign optimizes for an event the pixel has not seen yet | Trigger that event on your site, or report it with the Conversions API. |
| `400` with `param: "promotedObject.customEventType"` | A `conversions` goal with no event, or one Reddit does not support | Pick an event from the list above. |
| `400` with `param: "promotedObject.pixelId"` | The pixel belongs to a different ad account | Send a pixel owned by `adAccountId`. |
| `400` with `param: "linkUrl"` | `linkUrl` on a text ad | Drop the link, or switch to an image, video or carousel ad. |
| `400` with `param: "headline"` | No headline, or one longer than 300 characters | Add or trim the headline. |
| `400` with `param: "endDate"` | Lifetime budget, no end date | Add `endDate`. |
| `422` with `param: "budget"` | You tried to change the budget type, or set an ad group budget inside a campaign-budget campaign | Keep the current type, or edit the campaign's budget instead. |
| `403 reconnect_required` | The connection lacks `adsconversions` | Run the connect call again. |
| `429 rate_limited` | Reddit's per-user limit | Wait `Retry-After` seconds. |
| `502 platform_api_error` | Reddit refused the call | The message carries Reddit's own explanation. |

## Endpoints

| Endpoint | What it does |
| --- | --- |
| [`GET /v1/connect/{platform}/ads`](https://pozy.ai/docs/api/connectAds) | Connect ads for a platform |
| [`GET /v1/ads/accounts`](https://pozy.ai/docs/api/listAdAccounts) | List ad accounts |
| [`POST /v1/ads/create`](https://pozy.ai/docs/api/createStandaloneAd) | Create standalone ad |
| [`POST /v1/ads/boost`](https://pozy.ai/docs/api/boostPost) | Boost post as ad |
| [`POST /v1/ads/campaigns`](https://pozy.ai/docs/api/createAdCampaign) | Create a standalone campaign |
| [`GET /v1/ads/tree`](https://pozy.ai/docs/api/getAdTree) | Get campaign tree |
| [`GET /v1/ads`](https://pozy.ai/docs/api/listAds) | List ads |
| [`GET /v1/ads/{adId}`](https://pozy.ai/docs/api/getAd) | Get ad details |
| [`PUT /v1/ads/{adId}`](https://pozy.ai/docs/api/updateAd) | Update ad |
| [`PUT /v1/ads/{adId}/status`](https://pozy.ai/docs/api/updateAdStatus) | Pause or resume a single ad |
| [`PUT /v1/ads/ad-sets/{adSetId}`](https://pozy.ai/docs/api/updateAdSet) | Update an ad set |
| [`PUT /v1/ads/ad-sets/{adSetId}/status`](https://pozy.ai/docs/api/updateAdSetStatus) | Pause or resume a single ad set |
| [`GET /v1/ads/ad-sets/{adSetId}/targeting`](https://pozy.ai/docs/api/getAdSetTargeting) | Read a Reddit ad group's targeting and schedule |
| [`PUT /v1/ads/campaigns/{campaignId}`](https://pozy.ai/docs/api/updateAdCampaign) | Update a campaign |
| [`PUT /v1/ads/campaigns/{campaignId}/status`](https://pozy.ai/docs/api/updateAdCampaignStatus) | Pause or resume a campaign |
| [`DELETE /v1/ads/{adId}`](https://pozy.ai/docs/api/deleteAd) | Cancel an ad |
| [`DELETE /v1/ads/ad-sets/{adSetId}`](https://pozy.ai/docs/api/deleteAdSet) | Delete an ad set |
| [`DELETE /v1/ads/campaigns/{campaignId}`](https://pozy.ai/docs/api/deleteAdCampaign) | Delete a campaign |
| [`GET /v1/ads/{adId}/analytics`](https://pozy.ai/docs/api/getAdAnalytics) | Get ad analytics |
| [`GET /v1/ads/targeting/search`](https://pozy.ai/docs/api/searchAdTargeting) | Search targeting options |
| [`GET /v1/accounts/{accountId}/conversion-destinations`](https://pozy.ai/docs/api/listConversionDestinations) | List conversion destinations |
| [`POST /v1/ads/conversions`](https://pozy.ai/docs/api/sendConversions) | Send conversion events |

## Webhook events

| Event | When it fires |
| --- | --- |
| [`account.ads.initial_sync_completed`](https://pozy.ai/docs/webhooks/events#account-ads-initial-sync-completed) | Ads initial sync completed. |
| [`account.ads.sync_failed`](https://pozy.ai/docs/webhooks/events#account-ads-sync-failed) | Ads sync failed. |
| [`account.ads.sync_recovered`](https://pozy.ai/docs/webhooks/events#account-ads-sync-recovered) | Ads sync recovered. |
| [`ad.review.approved`](https://pozy.ai/docs/webhooks/events#ad-review-approved) | Ad review approved. |
| [`ad.review.rejected`](https://pozy.ai/docs/webhooks/events#ad-review-rejected) | Ad review rejected. |
