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). 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.
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"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);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:
{
"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.
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.
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"]
}
}'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);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"]){
"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):
{
"goal": "conversions",
"promotedObject": { "pixelId": "a2_gu5tcd9ah5se", "customEventType": "PURCHASE" }
}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.
dailyspends up tobudgetAmountper day;lifetimespends it over the flight and needsendDate. startDatedefaults to now. A date or time without an offset is read in the ad account's time zone, and a date-onlyendDateruns 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.
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:
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 read0on Reddit. - On connect, Pozy imports the last 90 days.
- Demographic
breakdownsare 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:
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, anexternalId, or the IP address with the user agent. eventTimeis Unix seconds. Reddit accepts events up to 7 days old.eventIdis Reddit's dedupe key against the same event fired by the pixel.- The connection needs
adsconversions. Without it the call returns403 reconnect_required.
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 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
#Webhook events
| Event | When it fires |
|---|---|
account.ads.initial_sync_completed |
Ads initial sync completed. |
account.ads.sync_failed |
Ads sync failed. |
account.ads.sync_recovered |
Ads sync recovered. |
ad.review.approved |
Ad review approved. |
ad.review.rejected |
Ad review rejected. |