| | 2 to 4 choices, 1 to 30 characters each. |
| `poll.duration` | `"ONE_DAY"` \| `"THREE_DAYS"` \| `"SEVEN_DAYS"` \| `"FOURTEEN_DAYS"` | `"SEVEN_DAYS"` | How long the poll accepts votes. |
| `geoRestriction` | \{countries\} | | Restrict visibility to up to 25 countries. Organization pages only, 300+ targeted followers. See [Geo-restriction](#geo-restriction). |
## Media requirements
### Images
| Property | Requirement |
|----------|-------------|
| Max images | 20 per post |
| Formats | JPEG, PNG, GIF |
| Max file size | 8 MB per image |
| Recommended | 1200 x 627 px |
| Min dimensions | 552 x 276 px |
| Max dimensions | 8192 x 8192 px |
| Type | Ratio | Dimensions | Use |
|------|-------|------------|-----|
| Landscape | 1.91:1 | 1200 x 627 px | Link shares, standard |
| Square | 1:1 | 1080 x 1080 px | Engagement |
| Portrait | 1:1.25 | 1080 x 1350 px | Mobile feed |
### Videos
| Property | Requirement |
|----------|-------------|
| Max videos | 1 per post |
| Formats | MP4, MOV, AVI |
| Max file size | 5 GB |
| Max duration | 10 minutes (personal), 30 minutes (organization page) |
| Min duration | 3 seconds |
| Resolution | 256 x 144 px to 4096 x 2304 px |
| Aspect ratio | 1:2.4 to 2.4:1 |
| Frame rate | 10 to 60 fps |
Recommended: 1920 x 1080 px, 16:9 or 1:1, 30 fps, H.264 with AAC audio at 192 kbps, 10 to 30 Mbps.
### Documents
| Property | Requirement |
|----------|-------------|
| Max documents | 1 per post |
| Formats | PDF, PPT, PPTX, DOC, DOCX |
| Max file size | 100 MB |
| Max pages | 300 |
### Media URLs
A media URL must be publicly accessible with no authentication, return the media bytes with the correct `Content-Type` header, not redirect to an HTML page, and sit on a fast host. Google Drive, Dropbox, OneDrive and iCloud links return an HTML page instead of the file and fail; test a URL in an incognito window, or upload through the [media endpoint](/guides/media-uploads). Zernio proxies Supabase storage URLs automatically.
## Analytics
Call `GET /v1/analytics?platform=linkedin` ([Analytics API](/analytics/get-analytics)). Personal accounts get metrics only for posts published through Zernio, because LinkedIn returns metrics only for posts authored by the authenticated member.
| Metric | Available |
|--------|-----------|
| Impressions | |
| Reach | |
| Likes | |
| Comments | |
| Shares | |
| Saves | (personal accounts only) |
| Sends | (personal accounts only, LinkedIn messaging) |
| Clicks | (organization accounts only) |
| Views | (video posts only) |
```typescript
const { data: analytics } = await zernio.analytics.getAnalytics({
query: { platform: 'linkedin', fromDate: '2026-08-01', toDate: '2026-08-31' }
});
console.log(analytics.posts);
```
```python
analytics = client.analytics.get_analytics(
platform="linkedin",
from_date="2026-08-01",
to_date="2026-08-31"
)
print(analytics["posts"])
```
```bash
curl "https://zernio.com/api/v1/analytics?platform=linkedin&fromDate=2026-08-01&toDate=2026-08-31" \
-H "Authorization: Bearer $ZERNIO_API_KEY"
```
Response (`200`), one entry per post:
```json
{
"posts": [
{
"_id": "65f1c0a9e2b5af0012ab34cd",
"platform": "linkedin",
"status": "published",
"publishedAt": "2026-08-14T10:00:05Z",
"platformPostUrl": "https://www.linkedin.com/feed/update/urn:li:share:7200000000000000000",
"analytics": {
"impressions": 8420,
"reach": 6210,
"likes": 131,
"comments": 17,
"shares": 9,
"clicks": 240,
"engagementRate": 1.86
}
}
]
}
```
Four LinkedIn-only endpoints go deeper:
- [Personal aggregate analytics](/analytics/get-linkedin-aggregate-analytics): totals or a daily series across a personal account's posts, covering impressions, reach, reactions, comments, shares, saves and sends.
- [Organization aggregate analytics](/analytics/get-linkedin-org-aggregate-analytics): impressions, clicks, reactions, engagement rate, follower gains (organic and paid) and page-view metrics for organization pages. The member must be an ADMINISTRATOR of the organization and the account must hold `r_organization_social`, `r_organization_followers` and `r_organization_admin`.
- [Post analytics](/analytics/get-linkedin-post-analytics): per-post metrics by URN for personal and organization accounts; saves and sends are personal-only, organizations return 0.
- [Post reactions](/analytics/get-linkedin-post-reactions): individual reactions with reactor profiles, organization accounts only.
## Inbox
LinkedIn supports comments on organization pages and reactions from either account type; its messaging API is closed to third-party apps, so there are no DMs.
| Feature | Supported |
|---------|-----------|
| List comments on posts | |
| Reply to comments | |
| Delete comments | |
| Like comments | (reactions, on accounts connected with the social-feed scopes) |
| React to a post | (any post, not only your own) |
[Like comment](/comments/like-inbox-comment) takes the composite comment URN the comments endpoints return as `commentId`, and an optional `reactionType` picks the reaction instead of the default `LIKE`. An account connected before Zernio requested the social-feed scopes gets a `403` with code `linkedin_reconnect_required`: reconnect it. [Like post](/comments/like-post) reacts to a post from any connected account, so an executive account can react to the brand's post; it takes the same `reactionType` and the same scopes. You can also read a post's reactions with [Post reactions](/analytics/get-linkedin-post-reactions) on organization accounts.
## What you cannot do
LinkedIn's API does not expose:
- Long-form articles (linkedin.com/article/new/)
- Events
- InMail and DMs
- Newsletters
- Following hashtags or tagging connections
- Mixed media in one post (images with a video, or images with a document)
- The one-click "Repost" feed treatment (reposts are always authored by your account)
## Common errors
| Error | Cause | Fix |
|-------|-------|-----|
| "Content is a duplicate of urn:li:share:XXXX" (`422`) | Identical or very similar content was already posted | Change the text meaningfully. LinkedIn's duplicate detection is strict, and a minor rephrase may not be enough. |
| "Publishing failed during preflight checks" | Rate limiting or validation caught an issue before publishing | Space posts further apart. Check [Account health](/accounts/get-all-accounts-health). |
| "Publishing failed due to max retries reached" | All 3 retry attempts failed | A temporary issue. Retry, or check LinkedIn's status page. |
| Token expired | The OAuth token expired or was revoked | Reconnect the account. Subscribe to the `account.disconnected` webhook to catch this early. |
| "Cannot mix media types" | The post has images with a video or a document | Use one media type per post. |
| Video processing failed | Codec, duration or aspect ratio is out of spec | H.264, within the duration limit (10 minutes personal, 30 minutes organization) and an aspect ratio between 1:2.4 and 2.4:1. |
| Link preview shows the wrong image | Missing or wrong Open Graph tags on the URL | Fix `og:image` on the site and refresh with the [LinkedIn Post Inspector](https://www.linkedin.com/post-inspector/). |
A `publishNow: true` post that LinkedIn rejects returns `207` with `post.status: "failed"` and the message in `platforms[].errorMessage`:
```json
{
"message": "Post created but publishing failed",
"error": "All platforms failed",
"post": {
"_id": "65f1c0a9e2b5af0012ab34cd",
"status": "failed",
"platforms": [
{
"platform": "linkedin",
"status": "failed",
"errorMessage": "Content is a duplicate of urn:li:share:7200000000000000000"
}
]
}
}
```
`207` is a 2xx status, so `fetch(...).ok` is `true`; branch on the status code and on `post.status`. [Error handling](/guides/error-handling) covers the envelope.
## Related
- [Connecting accounts](/guides/connecting-accounts): the OAuth flow and the organization selection step.
- [Create post](/posts/create-post) and [Edit post](/posts/edit-post): every field of the request.
- [LinkedIn mentions](/linkedin-mentions/get-linkedin-mentions): resolve profile and company URLs to mention URNs for post content.
- [Media uploads](/guides/media-uploads): upload images, videos and documents instead of hosting them.
- [Comments](/comments/list-inbox-comments): the inbox API.
- [Pricing](/pricing): what analytics, the inbox and outbound messages cost, and which replies count.
---
# OpenAI Ads
Run ChatGPT ads on an openaiads account, from connecting an API key to creating chat card campaigns, pixels and server-side conversions.
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
Create a ChatGPT chat card campaign with `POST /v1/ads/create` on an `openaiads` account, then measure it with a pixel and the server-side conversions endpoint. OpenAI Ads has no OAuth: you connect by posting an API key from ChatGPT Ads Manager, and one key covers exactly one ad account.
## Quick reference
| Property | Value |
|----------|-------|
| Hierarchy | Campaign > Ad group > Chat card ad, created in one call |
| Goals (`goal`) | `traffic`, `awareness`, `conversions` |
| Title (`headline`) | Required, 3 to 50 characters |
| Body | Required, up to 100 characters |
| Creative | `imageUrl` and `linkUrl`, both required, static image only |
| Budget | `budgetType: "lifetime"` with `endDate`, minimum $1 |
| Targeting | Country only |
| Bidding | Max bid, and a target CPA under the conversions goal |
| Conversion tracking | Pixel plus server-side Conversions API |
| Performance sync | Daily impressions, clicks and spend ([analytics](#analytics)) |
| Edits after creation | Status and budget only |
| Boost an existing post | No, ChatGPT has no organic posts |
| Custom audiences | No, OpenAI exposes audiences in Ads Manager only |
| Product feed upload | No, feeds are managed in Ads Manager over SFTP |
| Video creatives | No, chat cards are images |
## Before you start
Create the key in ChatGPT Ads Manager under Settings. It is scoped to one ad account, so connect each ad account separately. OpenAI opens Ads Manager to US-based businesses, and the ads serve to ChatGPT Free and Go users in the United States, Canada, Australia and New Zealand.
The key you paste carries full campaign write access, because OpenAI has no read-only key scope. Zernio uses it to read your ads and performance, and to create and manage the campaigns you set up in Zernio. Campaigns built in ChatGPT Ads Manager stay editable there.
## Connect
Call `POST /v1/connect/openai-ads/credentials` with the key and a `profileId`. Zernio validates the key against OpenAI before storing anything, and reposting a rotated key for the same ad account updates the connection in place. The endpoint also accepts the `x-connect-token` header, so you can hand the flow to your own customers like every other connect endpoint.
```typescript
import Zernio from '@zernio/node';
const zernio = new Zernio();
const { data: connected } = await zernio.connect.connectOpenAIAdsCredentials({
body: {
apiKey: 'sk-ads-xxxxxxxxxxxxxxxxxxxx',
profileId: '66a1f0c2a4b9d3e8f1a2b3c4'
}
});
console.log(connected.accountId);
```
```python
from zernio import Zernio
client = Zernio()
connected = client.connect.connect_open_ai_ads_credentials(
api_key="sk-ads-xxxxxxxxxxxxxxxxxxxx",
profile_id="66a1f0c2a4b9d3e8f1a2b3c4",
)
print(connected["accountId"])
```
```bash
curl -X POST "https://zernio.com/api/v1/connect/openai-ads/credentials" \
-H "Authorization: Bearer $ZERNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"apiKey": "sk-ads-xxxxxxxxxxxxxxxxxxxx",
"profileId": "66a1f0c2a4b9d3e8f1a2b3c4"
}'
```
Response (`200`):
```json
{
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"adAccountName": "Acme US"
}
```
A key that cannot read an OpenAI ad account returns `401` with code `invalid_credentials`. A `402` with `code: "PAYMENT_REQUIRED"` is a billing gate closing before the key is even checked, usually `reason: "free_tier_exceeded"`: the team is past its free connected accounts with no card on file, and `dashboard_url` in the body is where the end user adds one ([connect failures](/guides/connecting-accounts#if-it-fails)). The `accountId` is the `openaiads` account every sample below uses. Connecting from the Zernio dashboard, under Connections, does the same thing.
## Create a campaign
A chat card carries a title, a body, an image and a destination URL, so `headline`, `body`, `imageUrl` and `linkUrl` are all required. OpenAI has no daily budget, so `budgetType` must be `lifetime` and `endDate` gives the lifetime cap its spend window.
```typescript
const { data: created } = await zernio.adcampaigns.createStandaloneAd({
body: {
accountId: '66b2e19d8c3f5a7e9d0b1c2d',
adAccountId: 'acct_abc123',
name: 'Planner launch',
goal: 'traffic',
budgetAmount: 25,
budgetType: 'lifetime',
endDate: '2027-02-28T23:59:00Z',
bidStrategy: 'LOWEST_COST_WITH_BID_CAP',
bidAmount: 2,
headline: 'Try the new planner',
body: 'Coordinate tasks, docs and meetings in one place.',
imageUrl: 'https://cdn.example.com/planner.png',
linkUrl: 'https://example.com/planner',
countries: ['US']
}
});
console.log(created.ad._id);
```
```python
created = client.ad_campaigns.create_standalone_ad(
account_id="66b2e19d8c3f5a7e9d0b1c2d",
ad_account_id="acct_abc123",
name="Planner launch",
goal="traffic",
budget_amount=25,
budget_type="lifetime",
end_date="2027-02-28T23:59:00Z",
bid_strategy="LOWEST_COST_WITH_BID_CAP",
bid_amount=2,
headline="Try the new planner",
body="Coordinate tasks, docs and meetings in one place.",
image_url="https://cdn.example.com/planner.png",
link_url="https://example.com/planner",
countries=["US"],
)
print(created["ad"]["_id"])
```
```bash
curl -X POST "https://zernio.com/api/v1/ads/create" \
-H "Authorization: Bearer $ZERNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"adAccountId": "acct_abc123",
"name": "Planner launch",
"goal": "traffic",
"budgetAmount": 25,
"budgetType": "lifetime",
"endDate": "2027-02-28T23:59:00Z",
"bidStrategy": "LOWEST_COST_WITH_BID_CAP",
"bidAmount": 2,
"headline": "Try the new planner",
"body": "Coordinate tasks, docs and meetings in one place.",
"imageUrl": "https://cdn.example.com/planner.png",
"linkUrl": "https://example.com/planner",
"countries": ["US"]
}'
```
Response (`201`):
```json
{
"ad": {
"_id": "66f0a1b2c3d4e5f6a7b8c9d0",
"name": "Planner launch",
"platform": "openai",
"status": "pending_review",
"adType": "standalone",
"goal": "traffic",
"budget": { "amount": 25, "type": "lifetime" }
},
"message": "Ad created"
}
```
`budgetAmount` is in whole currency units, so `25` is $25.00, and the minimum is $1. Every sample below reuses the `zernio` and `client` constructors from this one.
### Goals and bidding
`traffic` bids on clicks, `awareness` on impressions and `conversions` on conversion events. The `conversions` goal needs an active conversion event setting on the ad account before the create call, from a [tracking tag](#conversion-tracking) with `defaultEventType` or from ChatGPT Ads Manager; without one the request returns `422`. Any other goal returns `400`.
OpenAI requires a bid cap on every ad group, so `bidStrategy` and `bidAmount` are required on the create: `LOWEST_COST_WITH_BID_CAP` and `COST_CAP` both map to the ad group's `bidding_config.max_bid_micros`, and `bidAmount` in whole currency units is converted to micros. There is no auto-bid option, so omitting `bidStrategy` or sending `LOWEST_COST_WITHOUT_CAP` returns `400` with code `missing_required_field`. `LOWEST_COST_WITH_MIN_ROAS` returns `422`, because OpenAI has no ROAS-based bidding.
### Targeting
Targeting is by country. Every other targeting field, including ages, genders, interests, `audienceInclude` and `audienceExclude`, returns `400` rather than being dropped silently.
## Manage ads
`PUT /v1/ads/{adId}` changes status and budget, and `PUT /v1/ads/{adId}/status` pauses or resumes a single ad, as on every other ads platform. A budget update stays lifetime-only. Sending `targeting` or `creative` returns `501` with code `unsupported_platform_operation`.
Deleting archives. OpenAI has no delete API, and archiving is terminal. Cancelling an ad in Zernio archives the ad, its ad group and its campaign on OpenAI, then marks the ad `cancelled` in Zernio.
## Conversion tracking
`POST /v1/accounts/{accountId}/tracking-tags` creates an OpenAI pixel and provisions a Conversions API key for it in one call. `adAccountId` is required by the endpoint and ignored here, because the API key already selects the ad account. Pass `defaultEventType` to provision the conversion event setting that `goal: "conversions"` needs; it takes one of `order_created`, `lead_created`, `items_added`, `contents_viewed`, `checkout_started`, `registration_completed`, `subscription_created`, `trial_started`, `appointment_scheduled`, `page_viewed`, `app_installed` or `app_opened`.
```typescript
const { data: createdTag } = await zernio.trackingtags.createTrackingTag({
path: { accountId: '66b2e19d8c3f5a7e9d0b1c2d' },
body: { adAccountId: 'acct_abc123', name: 'Acme website', defaultEventType: 'order_created' }
});
console.log(createdTag.tag.id);
```
```python
created_tag = client.tracking_tags.create_tracking_tag(
"66b2e19d8c3f5a7e9d0b1c2d",
ad_account_id="acct_abc123",
name="Acme website",
default_event_type="order_created",
)
print(created_tag["tag"]["id"])
```
```bash
curl -X POST "https://zernio.com/api/v1/accounts/66b2e19d8c3f5a7e9d0b1c2d/tracking-tags" \
-H "Authorization: Bearer $ZERNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "adAccountId": "acct_abc123", "name": "Acme website", "defaultEventType": "order_created" }'
```
Response (`201`):
```json
{
"platform": "openaiads",
"tag": {
"id": "134534000000",
"name": "Acme website",
"platform": "openaiads",
"status": "active"
}
}
```
Creating a tag is not idempotent and OpenAI pixels cannot be deleted, so never retry this call automatically: each call creates another pixel plus another Conversions API key. If the pixel is created but the key provisioning fails, the pixel stays live on OpenAI and the error names it. An ad account that is not enabled for pixel management returns `422` with code `FEATURE_NOT_AVAILABLE`; your OpenAI partner representative enables it.
### Send events
`POST /v1/ads/conversions` relays server-side events to OpenAI's Conversions API. `destinationId` is the pixel wire id, from `GET /v1/accounts/{accountId}/conversion-destinations` ([List conversion destinations](/conversions/list-conversion-destinations)). Zernio hashes email identifiers with SHA-256 and sends money in minor units, so send plaintext and whole currency values.
```typescript
const { data: sent } = await zernio.conversions.sendConversions({
body: {
accountId: '66b2e19d8c3f5a7e9d0b1c2d',
destinationId: '134534000000',
events: [{
eventName: 'Purchase',
eventTime: 1798804800,
eventId: 'order_12345',
sourceUrl: 'https://shop.example.com/checkout/confirmation',
value: 25.99,
currency: 'USD',
user: { email: 'customer@example.com' }
}]
}
});
console.log(sent.eventsReceived);
```
```python
sent = client.conversions.send_conversions(
account_id="66b2e19d8c3f5a7e9d0b1c2d",
destination_id="134534000000",
events=[{
"eventName": "Purchase",
"eventTime": 1798804800,
"eventId": "order_12345",
"sourceUrl": "https://shop.example.com/checkout/confirmation",
"value": 25.99,
"currency": "USD",
"user": {"email": "customer@example.com"},
}],
)
print(sent["eventsReceived"])
```
```bash
curl -X POST "https://zernio.com/api/v1/ads/conversions" \
-H "Authorization: Bearer $ZERNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"destinationId": "134534000000",
"events": [{
"eventName": "Purchase",
"eventTime": 1798804800,
"eventId": "order_12345",
"sourceUrl": "https://shop.example.com/checkout/confirmation",
"value": 25.99,
"currency": "USD",
"user": { "email": "customer@example.com" }
}]
}'
```
Response (`200`):
```json
{
"platform": "openaiads",
"eventsReceived": 1,
"eventsFailed": 0,
"failures": []
}
```
`Purchase`, `Lead`, `AddToCart`, `ViewContent`, `InitiateCheckout`, `CompleteRegistration`, `Subscribe`, `StartTrial` and `Schedule` map 1 to 1 onto OpenAI's own event types, so `Purchase` arrives as `order_created`, `Lead` as `lead_created` and `AddToCart` as `items_added`. Any other name is sent as a custom event with the name preserved. Larger submissions are split into chunks of 1,000 events, and each chunk is all-or-nothing: one malformed event fails its chunk, and `eventsFailed` plus `failures[]` name the events that did not land.
## Creative and account limits
| Limit | Value |
|-------|-------|
| Title | 3 to 50 characters |
| Body | Up to 100 characters |
| Budget | Lifetime only, minimum $1 |
| Targeting | Country only |
| Creative | Static image chat cards |
| Events per conversions request | 1,000 |
| OpenAI rate limits | 600 requests per minute per endpoint, 1,200 per minute overall |
## Analytics
Call `GET /v1/ads/{adId}/analytics` with the ad's `_id` ([Get ad analytics](/ad-insights/get-ad-analytics)). Zernio syncs impressions, clicks and spend from OpenAI once a day and derives CTR, CPC and CPM from them. `fromDate` and `toDate` default to the last 90 days.
```bash
curl "https://zernio.com/api/v1/ads/66f0a1b2c3d4e5f6a7b8c9d0/analytics?fromDate=2027-01-04&toDate=2027-01-11" \
-H "Authorization: Bearer $ZERNIO_API_KEY"
```
Response (`200`), trimmed:
```json
{
"ad": { "id": "66f0a1b2c3d4e5f6a7b8c9d0", "name": "Planner launch", "platform": "openai", "status": "active", "currency": "USD" },
"analytics": {
"summary": { "spend": 142.8, "impressions": 61200, "clicks": 940, "ctr": 1.54, "cpc": 0.15, "cpm": 2.33 },
"daily": [ { "date": "2027-01-04", "spend": 21.4, "impressions": 9100, "clicks": 138 } ]
}
}
```
Those 3 metrics are the whole sync: every other field of the response stays 0 on OpenAI, `reach` included, and the demographic `breakdowns` parameter is Meta and TikTok only. `backfillPending: true` means the history is still loading, so read it again shortly. `GET /v1/ads/tree` rolls the same metrics up per campaign and ad group.
## What you cannot do
OpenAI Ads through Zernio does not support:
- Boosting an existing post, because ChatGPT has no organic posts.
- Custom audiences, which OpenAI exposes in Ads Manager only.
- Product feed upload, which stays in Ads Manager over SFTP.
- Video creatives.
- Targeting or creative edits after creation, and any targeting beyond country.
- Deleting a pixel or an ad. Cancelling archives instead.
## Common errors
| Error | Cause | Fix |
|-------|-------|-----|
| `401` with code `invalid_credentials` | The pasted key cannot read an OpenAI ad account | Recreate the key in ChatGPT Ads Manager under Settings and post it again. |
| `422` on create | `goal: "conversions"` with no active conversion event setting | Create a tracking tag with `defaultEventType`, or configure the event in Ads Manager. |
| `422` on `budgetType: "daily"` | OpenAI has no daily budgets | Send `budgetType: "lifetime"` with an `endDate`. |
| `422` with code `TRACKING_TAG_REQUIRED` | Conversions sent before any pixel exists | Create the tracking tag first. |
| `422` with code `FEATURE_NOT_AVAILABLE` | The ad account is not enabled for pixel management | Ask your OpenAI partner representative to enable it. |
| `400` on a targeting field | Anything beyond `countries` | Remove the field; country is the only dimension OpenAI honours. |
| `501` with code `unsupported_platform_operation` | `targeting` or `creative` sent to `PUT /v1/ads/{adId}` | Change status or budget, and recreate the ad for anything else. |
A create with a daily budget returns the flat envelope:
```json
{
"error": "OpenAI Ads accepts lifetime budgets only",
"type": "invalid_request_error",
"code": "invalid_field_value",
"param": "budgetType"
}
```
[Error handling](/guides/error-handling) covers the envelope and the stable codes.
## Related
- [Connect an OpenAI Ads account](/connect/connect-open-aiads-credentials): every field of the connect call.
- [Create standalone ad](/ad-campaigns/create-standalone-ad): every field of the create request.
- [Create tracking tag](/tracking-tags/create-tracking-tag): pixels and Conversions API keys.
- [Send conversions](/conversions/send-conversions) and [List conversion destinations](/conversions/list-conversion-destinations).
- [Connecting accounts](/guides/connecting-accounts): profiles, accounts and the connect token.
---
# Pinterest Ads
Create Promoted Pin campaigns, promote Pins you already published, upload customer list audiences and read ad metrics on a pinterestads account.
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
Create a Promoted Pin campaign with `POST /v1/ads/create`, or promote a Pin you already published with `POST /v1/ads/boost`, on a `pinterestads` account. The ads account reuses the token of the Pinterest posting account in the same profile, so there is no second OAuth flow and no Pinterest developer application to file.
## Quick reference
| Property | Value |
|----------|-------|
| Hierarchy | Campaign > Ad Group > Promoted Pin, created in one call |
| Goals (`goal`) | `engagement`, `traffic`, `awareness`, `video_views` |
| Headline | Required, 100 characters |
| Body | Required, 500 characters |
| Creative | `imageUrl` and `linkUrl`, both required |
| Minimum budget | $5 (`budgetAmount`, or `budget.amount` on a boost) |
| Board | `boardId`, optional |
| Targeting | Country, region, metro, postal code, age, gender, interests |
| Audiences | Customer list |
| Analytics | Yes |
| Edits after creation | Status and budget only |
| Conversions API | Roadmap |
| Catalog and shopping ads | Roadmap |
## Before you start
You need a connected Pinterest account in the profile ([Pinterest](/platforms/pinterest)) and the Pinterest ad account id, from [List ad accounts](/ad-accounts/list-ad-accounts). Ads run on 2 scopes ([scopes](/guides/connecting-accounts#scopes)):
| Scope | What it enables |
|-------|-----------------|
| `ads:read` | Read ad accounts and reporting |
| `ads:write` | Create and manage campaigns, ad groups and Promoted Pins |
New Pinterest connections request both as part of the standard flow. An account connected before Pinterest Ads existed re-consents to these 2 scopes.
## Connect
Call `GET /v1/connect/pinterest/ads` with `profileId` ([Connect ads](/connect/connect-ads)). Pinterest shares one token between posting and ads, so when the profile already holds an active Pinterest account the `pinterestads` account inherits that token and no OAuth happens.
```bash
curl "https://zernio.com/api/v1/connect/pinterest/ads?profileId=66a1f0c2a4b9d3e8f1a2b3c4" \
-H "Authorization: Bearer $ZERNIO_API_KEY"
```
Response (`200`):
```json
{
"alreadyConnected": true,
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"platform": "pinterest",
"username": "@acmehome",
"displayName": "Acme Home"
}
```
Without an active Pinterest account the response carries `authUrl` and `state` instead; send the user to `authUrl`, as in the [connecting accounts guide](/guides/connecting-accounts). A Pinterest account whose stored token cannot reach ad accounts fails with `400 RECONNECT_REQUIRED`, which reconnecting the posting account clears. The `accountId` in the response is the `pinterestads` account every sample below uses.
Pinterest has no ads discovery, so the `adAccountId` and `adAccountIds` scoping parameters are accepted and ignored on this call. Pass the ad account per request instead.
## Create a Promoted Pin
Pinterest creates the Pin and promotes it in the same call, so `headline`, `body`, `imageUrl` and `linkUrl` are all required. `boardId` chooses the board; without it the Pin lands on a `Zernio Ads` board that Zernio creates once. `startDate` and `endDate` set the run window at the top level of the body, where the boost below takes a `schedule` object instead; omit them and the Promoted Pin runs until you pause it. An ad account that requires Campaign Budget Optimization needs `endDate` with a `lifetime` budget and returns a `400` naming `endDate` without one.
```typescript
import Zernio from '@zernio/node';
const zernio = new Zernio();
const { data: created } = await zernio.adcampaigns.createStandaloneAd({
body: {
accountId: '66b2e19d8c3f5a7e9d0b1c2d',
adAccountId: '549123456789',
name: 'Spring home decor',
goal: 'traffic',
budgetAmount: 30,
budgetType: 'daily',
headline: 'Freshen up your space',
body: 'Spring drops, limited time.',
imageUrl: 'https://cdn.example.com/spring-decor.jpg',
linkUrl: 'https://example.com/spring-decor',
countries: ['US', 'CA'],
ageMin: 25,
ageMax: 44
}
});
console.log(created.ad.platformAdSetId);
```
```python
from zernio import Zernio
client = Zernio()
created = client.ad_campaigns.create_standalone_ad(
account_id="66b2e19d8c3f5a7e9d0b1c2d",
ad_account_id="549123456789",
name="Spring home decor",
goal="traffic",
budget_amount=30,
budget_type="daily",
headline="Freshen up your space",
body="Spring drops, limited time.",
image_url="https://cdn.example.com/spring-decor.jpg",
link_url="https://example.com/spring-decor",
countries=["US", "CA"],
age_min=25,
age_max=44,
)
print(created["ad"]["platformAdSetId"])
```
```bash
curl -X POST "https://zernio.com/api/v1/ads/create" \
-H "Authorization: Bearer $ZERNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"adAccountId": "549123456789",
"name": "Spring home decor",
"goal": "traffic",
"budgetAmount": 30,
"budgetType": "daily",
"headline": "Freshen up your space",
"body": "Spring drops, limited time.",
"imageUrl": "https://cdn.example.com/spring-decor.jpg",
"linkUrl": "https://example.com/spring-decor",
"countries": ["US", "CA"],
"ageMin": 25,
"ageMax": 44
}'
```
Response (`201`):
```json
{
"ad": {
"_id": "66f0a1b2c3d4e5f6a7b8c9d0",
"name": "Spring home decor",
"platform": "pinterest",
"status": "pending_review",
"adType": "standalone",
"goal": "traffic",
"budget": { "amount": 30, "type": "daily" },
"platformAdId": "687195120340",
"platformCampaignId": "626746010001",
"platformAdSetId": "2680069880002"
},
"message": "Ad created"
}
```
`budgetAmount` is in whole currency units of the ad account, so `30` is $30.00 on a USD account. Every sample below reuses the `zernio` and `client` constructors from this one.
### Targeting fields
`countries`, `regions`, `metros`, `zips`, `ageMin`, `ageMax`, `gender` and `interests` (from `GET /v1/ads/interests`) apply on Pinterest, and `audienceInclude` and `audienceExclude` take the Pinterest customer list id of an audience. Pinterest's geo spec takes region codes or postal codes but never both, so sending `regions` and `zips` together returns a `400` with code `mutually_exclusive_fields`. `incomeTier` is rejected on Pinterest, and `cities`, `customLocations` and `behaviors` are Meta and TikTok only.
## Promote an existing Pin
`POST /v1/ads/boost` promotes a Pin that is already published, keeping its engagement. Pass the Zernio `postId` (or the Pinterest `platformPostId`), the account, the ad account, a goal and a budget.
```typescript
const { data: boosted } = await zernio.adcampaigns.boostPost({
body: {
postId: '65f1c0a9e2b5af0012ab34cd',
accountId: '66b2e19d8c3f5a7e9d0b1c2d',
adAccountId: '549123456789',
name: 'Boost decor Pin',
goal: 'traffic',
budget: { amount: 30, type: 'daily' },
schedule: { startDate: '2027-01-04T09:00:00Z', endDate: '2027-02-04T23:59:00Z' }
}
});
console.log(boosted.ad._id);
```
```python
boosted = client.ad_campaigns.boost_post(
post_id="65f1c0a9e2b5af0012ab34cd",
account_id="66b2e19d8c3f5a7e9d0b1c2d",
ad_account_id="549123456789",
name="Boost decor Pin",
goal="traffic",
budget={"amount": 30, "type": "daily"},
schedule={"startDate": "2027-01-04T09:00:00Z", "endDate": "2027-02-04T23:59:00Z"},
)
print(boosted["ad"]["_id"])
```
```bash
curl -X POST "https://zernio.com/api/v1/ads/boost" \
-H "Authorization: Bearer $ZERNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"postId": "65f1c0a9e2b5af0012ab34cd",
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"adAccountId": "549123456789",
"name": "Boost decor Pin",
"goal": "traffic",
"budget": { "amount": 30, "type": "daily" },
"schedule": { "startDate": "2027-01-04T09:00:00Z", "endDate": "2027-02-04T23:59:00Z" }
}'
```
Response (`201`):
```json
{
"ad": {
"_id": "66f0a1b2c3d4e5f6a7b8c9d1",
"name": "Boost decor Pin",
"platform": "pinterest",
"status": "pending_review",
"adType": "boost",
"goal": "traffic",
"budget": { "amount": 30, "type": "daily" }
},
"message": "Ad created"
}
```
Boosts are not idempotent. Send an `Idempotency-Key` header to make a retry replay the original `201` instead of creating a second ad ([idempotency](/guides/idempotency)).
## Customer list audiences
Create the audience with `POST /v1/ads/audiences` (`type: "customer_list"`), then upload members with `POST /v1/ads/audiences/{audienceId}/users`. Pinterest matches on email, ignores `phone`, and takes at most 10,000 users per request; Zernio hashes every value with SHA-256 before it leaves.
Pinterest needs the member file at creation time, so the audience is provisioned lazily: the create records it with status `pending` and no platform id, and the first upload creates it on Pinterest.
```typescript
const { data: audience } = await zernio.adaudiences.createAdAudience({
body: {
accountId: '66b2e19d8c3f5a7e9d0b1c2d',
adAccountId: '549123456789',
type: 'customer_list',
name: 'Repeat buyers'
}
});
const { data: upload } = await zernio.adaudiences.addUsersToAdAudience({
path: { audienceId: audience.audience.id },
body: { users: [{ email: 'jane@example.com' }, { email: 'sam@example.com' }] }
});
console.log(upload.numReceived);
```
```python
audience = client.ad_audiences.create_ad_audience(
account_id="66b2e19d8c3f5a7e9d0b1c2d",
ad_account_id="549123456789",
type="customer_list",
name="Repeat buyers",
)
upload = client.ad_audiences.add_users_to_ad_audience(
audience_id=audience["audience"]["id"],
users=[{"email": "jane@example.com"}, {"email": "sam@example.com"}],
)
print(upload["numReceived"])
```
```bash
curl -X POST "https://zernio.com/api/v1/ads/audiences" \
-H "Authorization: Bearer $ZERNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "accountId": "66b2e19d8c3f5a7e9d0b1c2d", "adAccountId": "549123456789", "type": "customer_list", "name": "Repeat buyers" }'
curl -X POST "https://zernio.com/api/v1/ads/audiences/66e5b2c3d4f5a6b7c8d9e0f1/users" \
-H "Authorization: Bearer $ZERNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "users": [{ "email": "jane@example.com" }, { "email": "sam@example.com" }] }'
```
Response (`201`):
```json
{
"audience": { "id": "66e5b2c3d4f5a6b7c8d9e0f1", "name": "Repeat buyers", "type": "customer_list", "platform": "pinterest", "status": "pending" },
"message": "Audience created"
}
```
Response (`200`):
```json
{ "message": "Users added", "numReceived": 2, "numInvalid": 0 }
```
## Media requirements
| Type | Formats | Max size | Notes |
|------|---------|----------|-------|
| Standard Pin | JPEG, PNG | 20 MB | 1000 x 1500 px (2:3) recommended |
| Video Pin | MP4, MOV, M4V | 2 GB | 4 seconds to 15 minutes, 9:16 or 1:1 |
Creatives are fetched from the URL you pass, which must be public and return the media bytes ([media uploads](/guides/media-uploads)).
## Analytics
Call `GET /v1/ads/{adId}/analytics` with the ad's `_id` for spend, impressions, clicks, CTR, CPC and CPM over a date range ([Get ad analytics](/ad-insights/get-ad-analytics)). Pinterest reports saves and closeups alongside those standard metrics.
```bash
curl "https://zernio.com/api/v1/ads/66f0a1b2c3d4e5f6a7b8c9d0/analytics?fromDate=2027-01-04&toDate=2027-01-11" \
-H "Authorization: Bearer $ZERNIO_API_KEY"
```
Response (`200`), trimmed:
```json
{
"ad": { "id": "66f0a1b2c3d4e5f6a7b8c9d0", "name": "Spring home decor", "platform": "pinterest", "status": "active", "currency": "USD" },
"analytics": {
"summary": { "spend": 186.5, "impressions": 74300, "clicks": 1240, "ctr": 1.67, "cpc": 0.15, "cpm": 2.51 },
"daily": [ { "date": "2027-01-04", "spend": 27.9, "impressions": 10600, "clicks": 181 } ]
}
}
```
Pinterest reports `reach` as 0, because Zernio does not sync it, and the demographic `breakdowns` parameter is Meta and TikTok only. `GET /v1/ads/tree` rolls the same metrics up per campaign and ad group ([Get campaign tree](/ad-campaigns/get-ad-tree)).
## What you cannot do
Pinterest Ads through Zernio does not support:
- Targeting or creative edits after creation. `PUT /v1/ads/{adId}` takes status and budget; `targeting` or `creative` returns `501` with code `unsupported_platform_operation`.
- Campaign duplication. `POST /v1/ads/campaigns/{campaignId}/duplicate` returns `501` on Pinterest.
- Ad-account scoping on the connect call, because Pinterest has no ads discovery.
- Income tier, city and radius targeting. Regions, metros and postal codes are supported.
- The Pinterest Conversions API, and catalog or shopping ads.
## Common errors
| Error | Cause | Fix |
|-------|-------|-----|
| `400` with code `missing_required_field` | One of `headline`, `body`, `imageUrl` or `linkUrl` is absent | Send all 4; Pinterest builds the Pin from them. |
| `400` with code `invalid_field_value` | A `goal` outside the 4 Pinterest values, or a headline over 100 or a body over 500 characters | Use a supported goal and trim the copy. |
| `403` with code `ads_allowance_exceeded` | The team has no payment method on file and has reached 500 live ads | Add a card to resume creating ads. |
| `501` with code `unsupported_platform_operation` | `targeting` or `creative` sent to `PUT /v1/ads/{adId}` | Change status or budget, and recreate the ad for anything else. |
| `502` with code `platform_api_error` | Pinterest rejected the request | Read `platformError` for Pinterest's own payload. |
A rejected create returns the flat envelope:
```json
{
"error": "headline is required",
"type": "invalid_request_error",
"code": "missing_required_field",
"param": "headline"
}
```
[Error handling](/guides/error-handling) covers the envelope and the stable codes.
## Related
- [Pinterest](/platforms/pinterest): the posting account this ads account inherits its token from.
- [Connecting accounts](/guides/connecting-accounts): the OAuth flow behind `authUrl`.
- [Create standalone ad](/ad-campaigns/create-standalone-ad) and [Boost post](/ad-campaigns/boost-post): every field of both requests.
- [Create ad audience](/ad-audiences/create-ad-audience) and [Add users to ad audience](/ad-audiences/add-users-to-ad-audience).
- [Get ad analytics](/ad-insights/get-ad-analytics): metrics, date ranges and rollups.
---
# Pinterest
Publish image and video pins to Pinterest boards with the Zernio API, with destination links, cover images, board creation and description edits.
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
Publish image and video pins to Pinterest with `POST /v1/posts` and `platform: "pinterest"`. The same account serves board management, description edits and analytics.
## Quick reference
| Property | Value |
|----------|-------|
| Title limit | 100 characters |
| Description limit | 800 characters, on create and on an [edit](#edit-a-published-pin) |
| Images per pin | 1 |
| Videos per pin | 1 |
| Image formats | JPEG, PNG, WebP, GIF |
| Image max size | 32 MB |
| Video formats | MP4, MOV |
| Video max size | 2 GB |
| Video duration | 4 seconds to 15 minutes |
| Scheduling | Yes |
| Editing published posts | Description only, 800 characters (blocked by Pinterest's closed beta) |
| Inbox | No (Pinterest has no API-accessible inbox) |
| Analytics | Yes (impressions, saves, clicks) |
## Before you start
Pinterest requires a board to pin to; create one in Pinterest or with [Create a board](#create-a-board). Every pin needs 1 image or 1 video, so there are no text-only pins and no carousels or multi-image posts. Pinterest is a search engine rather than a social feed: people find pins through search and browse, not through followers, so the title, description and board name matter more than the posting time, and a pin keeps surfacing for 3 to 6 months instead of hours. `link` is where a click on the pin sends people, so it is the field that drives traffic.
Send `boardId` on every post. It is optional, but a pin without one lands on whichever board Pinterest returns first, which is rarely the one you meant. When the account has no boards, or the board list cannot be read, the post fails with "Pinterest requires a boardId. Provide platformSpecificData.boardId." List the account's boards with `GET /v1/accounts/{accountId}/pinterest-boards`.
## Connect
Call `GET /v1/connect/pinterest` with `profileId` on [Get OAuth connect URL](/connect/get-connect-url). After OAuth the user picks a default board: [List boards](/connect/list-pinterest-boards-for-selection) then [Select board](/connect/select-pinterest-board), in standard or headless mode like the other selection platforms; see [platforms requiring secondary selection](/guides/connecting-accounts#platforms-requiring-secondary-selection). Change the default board later with [Update Pinterest board](/connect/update-pinterest-boards), with no second OAuth. The [connecting accounts guide](/guides/connecting-accounts) covers the flow and [scopes](/guides/connecting-accounts#scopes) in general; [Account health](/accounts/get-all-accounts-health) reports what a connected account can do with the scopes the user granted.
### OAuth scopes
| Scope | What it enables |
|-------|-----------------|
| `boards:read` | List boards during connection and post creation |
| `boards:write` | [Create boards](#create-a-board) through the API |
| `pins:read` | Read pins (permalinks, analytics) |
| `pins:write` | Create pins |
| `user_accounts:read` | Account identity and account analytics |
| `ads:read`, `ads:write` | [Pinterest Ads](/platforms/pinterest-ads): read ad accounts, create and manage campaigns |
## Publish
A plain post becomes an image pin when the media is an image and a video pin when it is a video. `boardId`, `title` and `link` go in `platformSpecificData` on the Pinterest entry; `content` is the pin description.
### Image pin
A single image pinned to a board. Use a 2:3 aspect ratio (1000 x 1500 px) for the best display in the Pinterest feed.
```typescript
import Zernio from '@zernio/node';
const zernio = new Zernio();
const { data: published } = await zernio.posts.createPost({
body: {
content: 'Modern kitchen renovation ideas for small spaces',
mediaItems: [
{ type: 'image', url: 'https://cdn.example.com/kitchen-ideas.jpg' }
],
platforms: [{
platform: 'pinterest',
accountId: '66b2e19d8c3f5a7e9d0b1c2d',
platformSpecificData: {
title: 'Modern Kitchen Renovation Ideas',
boardId: '1022106146582011234',
link: 'https://myblog.com/kitchen-renovation'
}
}],
publishNow: true
}
});
console.log(published.post.platforms[0].platformPostUrl);
```
```python
from zernio import Zernio
client = Zernio()
published = client.posts.create_post(
content="Modern kitchen renovation ideas for small spaces",
media_items=[
{"type": "image", "url": "https://cdn.example.com/kitchen-ideas.jpg"}
],
platforms=[{
"platform": "pinterest",
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"platformSpecificData": {
"title": "Modern Kitchen Renovation Ideas",
"boardId": "1022106146582011234",
"link": "https://myblog.com/kitchen-renovation"
}
}],
publish_now=True
)
print(published["post"]["platforms"][0]["platformPostUrl"])
```
```bash
curl -X POST https://zernio.com/api/v1/posts \
-H "Authorization: Bearer $ZERNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "Modern kitchen renovation ideas for small spaces",
"mediaItems": [
{"type": "image", "url": "https://cdn.example.com/kitchen-ideas.jpg"}
],
"platforms": [{
"platform": "pinterest",
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"platformSpecificData": {
"title": "Modern Kitchen Renovation Ideas",
"boardId": "1022106146582011234",
"link": "https://myblog.com/kitchen-renovation"
}
}],
"publishNow": true
}'
```
Response (`201`):
```json
{
"post": {
"_id": "65f1c0a9e2b5af0012ab34cd",
"status": "published",
"platforms": [
{
"platform": "pinterest",
"status": "published",
"platformPostUrl": "https://www.pinterest.com/pin/..."
}
]
}
}
```
Every sample below changes only the `mediaItems` or the `platforms` entry of this request.
### Video pin
A single video pinned to a board. `coverImageUrl` sets a custom cover; `coverImageKeyFrameTime` extracts a frame at that second instead.
```json
"mediaItems": [
{ "type": "video", "url": "https://cdn.example.com/recipe.mp4" }
],
"platforms": [{
"platform": "pinterest",
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"platformSpecificData": {
"title": "5-Minute Breakfast Recipe",
"boardId": "1022106146582011234",
"link": "https://myrecipes.com/quick-breakfast",
"coverImageUrl": "https://cdn.example.com/recipe-cover.jpg"
}
}]
```
### Boards
Every pin belongs to a board. List the boards on a connected account, or create one through the API; the `boards:write` scope is part of every Pinterest connection, so no reconnect is needed.
#### List boards
```typescript
const { data: boards } = await zernio.connect.getPinterestBoards({
path: { accountId: '66b2e19d8c3f5a7e9d0b1c2d' }
});
console.log(boards.boards);
```
```python
boards = client.connect.get_pinterest_boards(account_id="66b2e19d8c3f5a7e9d0b1c2d")
print(boards["boards"])
```
```bash
curl "https://zernio.com/api/v1/accounts/66b2e19d8c3f5a7e9d0b1c2d/pinterest-boards" \
-H "Authorization: Bearer $ZERNIO_API_KEY"
```
Response (`200`):
```json
{
"boards": [
{
"id": "1022106146582011234",
"name": "Kitchen Ideas",
"description": "Renovations and layouts",
"privacy": "PUBLIC",
"url": "https://www.pinterest.com/yourhandle/kitchen-ideas/"
}
]
}
```
#### Create a board
The response carries the new board's `id`, which works at once as `platformSpecificData.boardId`. `privacy` is `PUBLIC` (default), `PROTECTED` or `SECRET`.
```typescript
const { data: created } = await zernio.connect.createPinterestBoard({
path: { accountId: '66b2e19d8c3f5a7e9d0b1c2d' },
body: {
name: 'Summer Recipes',
description: 'My favorite summer recipes',
privacy: 'PUBLIC'
}
});
console.log(created.board.id);
```
```python
created = client.connect.create_pinterest_board(
account_id="66b2e19d8c3f5a7e9d0b1c2d",
name="Summer Recipes",
description="My favorite summer recipes",
privacy="PUBLIC",
)
print(created["board"]["id"])
```
```bash
curl -X POST "https://zernio.com/api/v1/accounts/66b2e19d8c3f5a7e9d0b1c2d/pinterest-boards" \
-H "Authorization: Bearer $ZERNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Summer Recipes",
"description": "My favorite summer recipes",
"privacy": "PUBLIC"
}'
```
Response (`201`):
```json
{
"board": {
"id": "1022106146582019876",
"name": "Summer Recipes",
"description": "My favorite summer recipes",
"privacy": "PUBLIC",
"url": "https://www.pinterest.com/yourhandle/summer-recipes/"
}
}
```
Pinterest rejects a duplicate board name on the same account, and you cannot pin to a secret board you do not own. A board-creation error from Pinterest comes back with its original message and a `502`.
### Edit a published pin
[Edit post](/posts/edit-post) replaces the description of a pin that is already live, up to the same 800 characters a create takes. The title, media, destination `link` and board cannot be changed, a title derived from the first line of the original content at publish time stays as it was, and there is no time window, no edit limit and the pin id is unchanged.
Pinterest's pin-update endpoint is a closed beta. Until Pinterest allowlists the app, every edit request is rejected with a "beta feature not yet enabled" error. Nothing on your side changes that; Pinterest grants the access.
```typescript
const { data: edited } = await zernio.posts.editPost({
path: { postId: '65f1c0a9e2b5af0012ab34cd' },
body: {
platform: 'pinterest',
content: 'Updated pin description with the new recipe link details'
}
});
console.log(edited.id, edited.url);
```
```python
edited = client.posts.edit_post(
post_id="65f1c0a9e2b5af0012ab34cd",
platform="pinterest",
content="Updated pin description with the new recipe link details"
)
print(edited["id"], edited["url"])
```
```bash
curl -X POST https://zernio.com/api/v1/posts/65f1c0a9e2b5af0012ab34cd/edit \
-H "Authorization: Bearer $ZERNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"platform": "pinterest",
"content": "Updated pin description with the new recipe link details"
}'
```
Response (`200`):
```json
{
"success": true,
"id": "1022106146582013456",
"url": "https://www.pinterest.com/pin/1022106146582013456/",
"message": "Post edited successfully"
}
```
If the post was published to several Pinterest accounts, pass `accountId` to pick which copy to edit; without it, the first `pinterest` entry on the post is edited.
## Platform fields
All fields go in `platformSpecificData` on the Pinterest entry.
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `boardId` | string | | The board to pin to. Send it on every post. Get board ids from `GET /v1/accounts/{accountId}/pinterest-boards`, or [create a board](#create-a-board) with `POST` on the same path. |
| `boardSectionId` | string | | Section inside the board. The pin lands on the board itself when omitted. Pinterest rejects the pin if the section does not belong to `boardId`, so send both together. No Zernio endpoint returns section ids: read them from Pinterest's own board sections API, which the same connected account can call. |
| `title` | string (max 100 chars) | First line of content | Pin title, searchable by Pinterest users. |
| `link` | string (URL) | | Destination when someone clicks the pin. Must be valid HTTPS, no URL shorteners. The field that drives traffic. Alias: `url`. |
| `coverImageUrl` | string (URL) | | Custom cover image for video pins. |
| `coverImageKeyFrameTime` | number (seconds) | `0` | Extract a video frame at this many seconds as the cover. Ignored when `coverImageUrl` is set. |
| `isAiGenerated` | boolean | `false` | `true` creates the pin with Pinterest's AI_MODIFIED disclosure (an "AI modified" label). `false` omits the disclosure; Pinterest may still label a pin from its own detection. |
## Media requirements
### Images
| Property | Requirement |
|----------|-------------|
| Max images | 1 per pin |
| Formats | JPEG, PNG, WebP, GIF |
| Max file size | 32 MB |
| Recommended | 1000 x 1500 px (2:3) |
| Min dimensions | 100 x 100 px |
| Ratio | Dimensions | Use |
|-------|------------|-----|
| 2:3 | 1000 x 1500 px | Standard pin, the best display in the feed |
| 1:1 | 1000 x 1000 px | Square pin |
| 1:2.1 | 1000 x 2100 px | Long pin (max height) |
Animated GIFs auto-play in the feed and are treated as images, not video. The limit is 32 MB, and under 10 MB loads fast.
### Videos
| Property | Requirement |
|----------|-------------|
| Max videos | 1 per pin |
| Formats | MP4, MOV |
| Max file size | 2 GB |
| Duration | 4 seconds to 15 minutes |
| Aspect ratio | 2:3, 1:1 or 9:16 |
| Resolution | 1080p recommended |
| Frame rate | 25 fps or more |
| Property | Minimum | Recommended |
|----------|---------|-------------|
| Resolution | 240p | 1080p |
| Bitrate | | 10 Mbps |
| Audio | | AAC, 128 kbps |
### Media URLs
A media URL must be publicly accessible with no authentication, return the media bytes with the correct `Content-Type` header, not redirect to an HTML page, and sit on a fast host. Google Drive, Dropbox, OneDrive and iCloud sharing links return a webpage and fail. To host files instead, use the [media endpoint](/guides/media-uploads).
## Analytics
Call `GET /v1/analytics?platform=pinterest` ([Analytics API](/analytics/get-analytics)).
| Metric | Available |
|--------|-----------|
| Impressions | |
| Saves | |
| Clicks | |
```typescript
const { data: analytics } = await zernio.analytics.getAnalytics({
query: { platform: 'pinterest', fromDate: '2026-08-01', toDate: '2026-08-31' }
});
console.log(analytics.posts);
```
```python
analytics = client.analytics.get_analytics(
platform="pinterest",
from_date="2026-08-01",
to_date="2026-08-31"
)
print(analytics["posts"])
```
```bash
curl "https://zernio.com/api/v1/analytics?platform=pinterest&fromDate=2026-08-01&toDate=2026-08-31" \
-H "Authorization: Bearer $ZERNIO_API_KEY"
```
Response (`200`), one entry per post:
```json
{
"posts": [
{
"_id": "65f1c0a9e2b5af0012ab34cd",
"platform": "pinterest",
"status": "published",
"publishedAt": "2026-08-14T10:00:05Z",
"platformPostUrl": "https://www.pinterest.com/pin/1022106146582013456/",
"analytics": {
"impressions": 4380,
"saves": 96,
"clicks": 141
}
}
]
}
```
## Inbox
Pinterest has no inbox API: no DMs, no comments and no reviews.
## What you cannot do
Pinterest's API does not expose:
- Idea Pins (multi-page stories)
- Claiming a website
- Rich Pins (they need meta tags on your website)
- Shopping catalogs
- Changing a published pin's media, destination link or board (only the description; see [Edit a published pin](#edit-a-published-pin))
- Multi-image posts or carousels
## Common errors
| Error | Cause | Fix |
|-------|-------|-----|
| "Invalid URL or request data." | Pinterest could not process the URL or request data | Check that the media URL is public, returns media bytes and uses HTTPS. |
| "Unable to reach the URL. Please check the URL is correct and try again." | Pinterest's servers cannot fetch the media | Test the URL in an incognito window. No authentication, no redirect to an HTML page. |
| "Pinterest rate limit reached." | Too many API calls in a short window | Space out pins; avoid bursts of 10 or more at once. |
| "Pinterest requires a boardId. Provide platformSpecificData.boardId." | No board in the request | Send `boardId`. List boards with `GET /v1/accounts/{accountId}/pinterest-boards`. |
A `publishNow: true` post that Pinterest rejects returns `207` with `post.status: "failed"` and the message in `platforms[].errorMessage`:
```json
{
"message": "Post created but publishing failed",
"error": "All platforms failed",
"post": {
"_id": "65f1c0a9e2b5af0012ab34cd",
"status": "failed",
"platforms": [
{
"platform": "pinterest",
"status": "failed",
"errorMessage": "Unable to reach the URL. Please check the URL is correct and try again."
}
]
}
}
```
`207` is a 2xx status, so `fetch(...).ok` is `true`; branch on the status code and on `post.status`. [Error handling](/guides/error-handling) covers the envelope.
## Related
- [Connecting accounts](/guides/connecting-accounts): the OAuth flow and board selection.
- [Create post](/posts/create-post): every field of the request.
- [Edit post](/posts/edit-post): update the description of a published pin.
- [Media uploads](/guides/media-uploads): upload images and videos instead of hosting them.
- [Pinterest boards](/connect/get-pinterest-boards) and [Create Pinterest board](/connect/create-pinterest-board): the boards endpoints.
---
# Reddit
Publish text, link, image, gallery and native video posts to a subreddit with the Zernio API, with flairs, NSFW and spoiler tags.
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
Publish text, link, image, gallery and native video posts to Reddit with `POST /v1/posts` and `platform: "reddit"`. Every post targets one subreddit, and each subreddit has its own rules, so read them before you submit. The same account also serves DMs, comments and subreddit search.
## Quick reference
| Property | Value |
|----------|-------|
| Title limit | 300 characters (required, cannot be edited after posting) |
| Body text | 40,000 characters |
| Images per post | 1 (single), 2 to 20 (gallery) |
| Videos per post | 1 |
| Video format | MP4 |
| Video max size | 1 GB |
| Image formats | JPEG, PNG, GIF |
| Image max size | 20 MB |
| Post types | Text, Link, Image, Gallery, Native video |
| Scheduling | Yes |
| Inbox (DMs) | Yes (text only) |
| Inbox (comments) | Yes |
| Editing published posts | Body of a text post; titles and link posts cannot be edited |
| Analytics | Limited (upvotes and comments only) |
## Before you start
Reddit requires a subreddit for every post, and each subreddit is moderated independently with its own rules. What one subreddit accepts, another removes. Before posting to a subreddit through the API, check that it allows your post type (text, link or image), whether it requires a flair (many subreddits auto-remove posts without one), whether it allows third-party or automated posting, and its karma and account-age requirements. [Get subreddit rules](/connect/get-subreddit-rules) returns a subreddit's posting rules plus Reddit's site-wide rules, and [Check subreddit](/validate/validate-subreddit) confirms that a subreddit exists and which post types it allows.
The title is permanent: Reddit titles cannot be edited after posting. New accounts are restricted: low karma and a young account block most subreddits. Video rules vary by subreddit: native video with body text fails if the subreddit blocks videos. Video posts without body text can fall back to a link post.
## Connect
Call `GET /v1/connect/reddit` with `profileId` on [Get OAuth connect URL](/connect/get-connect-url). The [connecting accounts guide](/guides/connecting-accounts) covers the OAuth flow and [scopes](/guides/connecting-accounts#scopes); [Account health](/accounts/get-all-accounts-health) reports what a connected account can do with the scopes the user granted.
A connected account has a default subreddit. [List Reddit subreddits](/connect/get-reddit-subreddits) returns the subreddits the account can post to and the current default, and [Set default subreddit](/connect/update-reddit-subreddits) changes it without a second OAuth.
### OAuth scopes
| Scope | What it enables |
|-------|-----------------|
| `identity` | Account identity (username, avatar) |
| `submit` | Submit posts and comments |
| `read` | Read posts and comments (analytics and comment fetching) |
| `mysubreddits` | List subreddits the user is subscribed to or moderates |
| `flair` | Read and set post flair |
| `history` | Fetch post and comment history |
| `privatemessages` | Reddit private messages in the inbox |
| `edit` | Edit and delete the account's own posts and comments |
| `vote` | Vote on posts and comments |
## Publish
A plain post becomes a text post (a self post) in the account's default subreddit. The first line of `content` becomes the title, the rest becomes the body, and Reddit Markdown works in the body. Fields in `platformSpecificData` on the Reddit entry pick the subreddit, turn the post into a link post, and set flair, NSFW and spoiler tags.
### Text post
```typescript
import Zernio from '@zernio/node';
const zernio = new Zernio();
const { data: published } = await zernio.posts.createPost({
body: {
content: 'Tips for learning a new programming language\n\nHere is what worked for me:\n\n1. Start with the official tutorial\n2. Build a small project immediately\n3. Read other people\'s code',
platforms: [
{
platform: 'reddit',
accountId: '66b2e19d8c3f5a7e9d0b1c2d',
platformSpecificData: { subreddit: 'learnprogramming' }
}
],
publishNow: true
}
});
console.log(published.post.platforms[0].platformPostUrl);
```
```python
from zernio import Zernio
client = Zernio()
published = client.posts.create_post(
content="Tips for learning a new programming language\n\nHere is what worked for me:\n\n1. Start with the official tutorial\n2. Build a small project immediately\n3. Read other people's code",
platforms=[
{
"platform": "reddit",
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"platformSpecificData": {"subreddit": "learnprogramming"}
}
],
publish_now=True
)
print(published["post"]["platforms"][0]["platformPostUrl"])
```
```bash
curl -X POST https://zernio.com/api/v1/posts \
-H "Authorization: Bearer $ZERNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "Tips for learning a new programming language\n\nHere is what worked for me:\n\n1. Start with the official tutorial\n2. Build a small project immediately\n3. Read other people'\''s code",
"platforms": [
{
"platform": "reddit",
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"platformSpecificData": {"subreddit": "learnprogramming"}
}
],
"publishNow": true
}'
```
Response (`201`):
```json
{
"post": {
"_id": "65f1c0a9e2b5af0012ab34cd",
"status": "published",
"platforms": [
{
"platform": "reddit",
"status": "published",
"platformPostUrl": "https://www.reddit.com/r/learnprogramming/comments/..."
}
]
}
}
```
Every sample below changes only the `mediaItems` or the `platforms` entry of this request.
### Link post
`url` turns the post into a link post; `content` becomes the title:
```json
{
"platform": "reddit",
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"platformSpecificData": {
"subreddit": "programming",
"url": "https://example.com/api-design-article"
}
}
```
When a link post fails with `NO_LINKS` because the subreddit allows text posts only, Zernio retries it as a text post with the URL in the body.
### Image post
A single image; `content` becomes the title:
```json
"mediaItems": [
{ "type": "image", "url": "https://cdn.example.com/hiking-photo.jpg" }
]
```
### Gallery
2 or more images publish as a gallery, up to 20. Images past the 20th are dropped, without an error, so trim the list yourself when you have more. Not every subreddit allows galleries:
```json
"mediaItems": [
{ "type": "image", "url": "https://cdn.example.com/step1.jpg" },
{ "type": "image", "url": "https://cdn.example.com/step2.jpg" },
{ "type": "image", "url": "https://cdn.example.com/step3.jpg" },
{ "type": "image", "url": "https://cdn.example.com/finished.jpg" }
]
```
### Native video
A video uploads to Reddit's CDN and publishes as a native video that plays in Reddit's embedded player. Reddit transcodes it server-side with a 1080p and 30 fps cap. The post `content`, or the Reddit entry’s `customContent` override, becomes the original video post’s Markdown body. If body text is present, upload failures and subreddit video restrictions fail the post without converting it to a link. Automatic link fallback applies only to videos without body text. `nativeVideo: false` skips the upload and posts the video URL as an external link instead:
```json
"mediaItems": [
{ "type": "video", "url": "https://cdn.example.com/demo.mp4" }
]
```
For a native video with an explanation and CTA URL, send a request like this to `POST /v1/posts`:
```json
{
"content": "How the product works\n\nTry it: https://example.com/product",
"mediaItems": [
{ "type": "video", "url": "https://cdn.example.com/demo.mp4" }
],
"platforms": [
{
"platform": "reddit",
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"platformSpecificData": {
"subreddit": "your_community",
"title": "A short product demo",
"nativeVideo": true
}
}
],
"publishNow": true
}
```
Leave `forceSelf` unset for this combination. `forceSelf: true` creates a text-only self post and skips native media uploads. Adding body text does not bypass a subreddit's video restrictions.
Zernio extracts the video's first frame as the poster. `videoPosterUrl` replaces it:
```json
{
"platform": "reddit",
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"platformSpecificData": {
"subreddit": "videos",
"videoPosterUrl": "https://cdn.example.com/poster.jpg"
}
}
```
`videogif: true` submits the same upload as a silent looping clip. The video is still uploaded natively; only Reddit's kind changes:
```json
{
"platform": "reddit",
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"platformSpecificData": { "subreddit": "gifs", "videogif": true }
}
```
### Flair
Some subreddits require a post flair. List the flairs with [List subreddit flairs](/connect/get-reddit-flairs), then pass the `id` as `flairId`:
```typescript
const { data: flairs } = await zernio.connect.getRedditFlairs({
path: { accountId: '66b2e19d8c3f5a7e9d0b1c2d' },
query: { subreddit: 'socialmedia' }
});
console.log(flairs.flairs);
```
```python
flairs = client.connect.get_reddit_flairs(
account_id="66b2e19d8c3f5a7e9d0b1c2d",
subreddit="socialmedia"
)
print(flairs["flairs"])
```
```bash
curl "https://zernio.com/api/v1/accounts/66b2e19d8c3f5a7e9d0b1c2d/reddit-flairs?subreddit=socialmedia" \
-H "Authorization: Bearer $ZERNIO_API_KEY"
```
Response (`200`):
```json
{
"flairs": [
{ "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "text": "Discussion", "textColor": "dark", "backgroundColor": "#edeff1" },
{ "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "text": "News", "textColor": "light", "backgroundColor": "#ff4500" }
]
}
```
```json
{
"platform": "reddit",
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"platformSpecificData": {
"subreddit": "socialmedia",
"flairId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
}
```
If the subreddit requires a flair and the request has no `flairId`, Zernio uses the first available flair. To change the flair of a post that is already live, call [Set Reddit post flair](/connect/set-reddit-post-flair).
### NSFW and spoiler tags
`nsfw: true` marks the post as over 18. `spoiler: true` marks it as a spoiler, which takes effect only when the subreddit has spoiler tagging enabled:
```json
{
"platform": "reddit",
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"platformSpecificData": { "subreddit": "television", "spoiler": true }
}
```
### Edit a published post
`POST /v1/posts/{postId}/edit` with `platform: "reddit"` and the new `content` replaces the body of a published text post. Reddit keeps the same post id, and there is no time window. A link post has no editable body and is rejected with a `400` before the write; the title can never be edited.
```bash
curl -X POST "https://zernio.com/api/v1/posts/65f1c0a9e2b5af0012ab34cd/edit" \
-H "Authorization: Bearer $ZERNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "platform": "reddit", "content": "Updated write-up, with the benchmark numbers." }'
```
Response (`200`):
```json
{
"success": true,
"id": "1abcd2",
"url": "https://www.reddit.com/r/learnprogramming/comments/1abcd2/tips/",
"message": "reddit post edited successfully"
}
```
## Platform fields
All fields go in `platformSpecificData` on the Reddit entry.
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `subreddit` | string | account default | Target subreddit without the `r/` prefix. |
| `title` | string | first line of `content` | Post title, at most 300 characters. Cannot be edited after posting. |
| `url` | string (URL) | | Makes a link post instead of a text post. |
| `forceSelf` | boolean | `false` | Makes a text-only self post and skips native media uploads, even when media is provided. Omit for native video with body text. |
| `nativeVideo` | boolean | `true` | Uploads video to Reddit's CDN for the embedded player. `false` explicitly publishes an external link instead. When body text is present, upload failures or subreddit video restrictions fail the post without falling back to a link. |
| `videogif` | boolean | `false` | Submits the native video as a silent looping clip. |
| `videoPosterUrl` | string (URL) | first frame | Poster image for a native video. |
| `flairId` | string | | Flair id from [List subreddit flairs](/connect/get-reddit-flairs). Required by some subreddits. |
| `flairText` | string | | Free-text flair for subreddits that allow it. Ignored when `flairId` is set. |
| `nsfw` | boolean | `false` | Marks the post as over 18. |
| `spoiler` | boolean | `false` | Marks the post as a spoiler. The subreddit must have spoiler tagging enabled. |
| `sendreplies` | boolean | `true` | `false` stops comment replies from reaching the account's Reddit inbox. |
## Media requirements
Images are limited to 20 MB.
### Images
| Property | Requirement |
|----------|-------------|
| Max images | 1 (single), 2 to 20 (gallery) |
| Formats | JPEG, PNG, GIF |
| Max file size | 20 MB |
| Recommended | 1200 x 628 px |
Reddit accepts 16:9, 4:3, 1:1 and 9:16 images. GIFs display as a still frame until clicked, may be converted to video by Reddit, and load faster under 10 MB.
### Videos
| Property | Requirement |
|----------|-------------|
| Max videos | 1 per post |
| Format | MP4 |
| Max file size | 1 GB |
| Resolution | Transcoded server-side, capped at 1080p and 30 fps |
Upload at 1080p or below, because Reddit re-encodes anything larger anyway. 1 GB is Reddit's own cap, and a file above it is too large for Zernio to compress, so it reaches Reddit unchanged and Reddit refuses it. Reddit publishes no maximum duration for an API upload, so file size is the limit that binds. Hosting rules for media URLs are in [Media uploads](/guides/media-uploads).
## Analytics
Call `GET /v1/analytics?platform=reddit` ([Analytics API](/analytics/get-analytics)). Reddit exposes the score and the comment count only, so analytics are limited to these two metrics:
| Metric | Available |
|--------|-----------|
| Likes (upvotes) | |
| Comments | |
Reddit's API does not provide impressions, reach, shares, clicks or view counts.
```typescript
const { data: analytics } = await zernio.analytics.getAnalytics({
query: { platform: 'reddit', fromDate: '2026-08-01', toDate: '2026-08-31' }
});
console.log(analytics.posts);
```
```python
analytics = client.analytics.get_analytics(
platform="reddit",
from_date="2026-08-01",
to_date="2026-08-31"
)
print(analytics["posts"])
```
```bash
curl "https://zernio.com/api/v1/analytics?platform=reddit&fromDate=2026-08-01&toDate=2026-08-31" \
-H "Authorization: Bearer $ZERNIO_API_KEY"
```
Response (`200`), one entry per post:
```json
{
"posts": [
{
"_id": "65f1c0a9e2b5af0012ab34cd",
"platform": "reddit",
"status": "published",
"publishedAt": "2026-08-14T10:00:05Z",
"platformPostUrl": "https://www.reddit.com/r/learnprogramming/comments/1abcd2/tips/",
"analytics": {
"likes": 342,
"comments": 28
}
}
]
}
```
## Inbox
Reddit supports DMs (Reddit private messages) and comments. DMs are text only, because Reddit's API does not support attachments in private messages.
### Direct messages
| Feature | Supported |
|---------|-----------|
| List conversations | |
| Fetch messages | |
| Send text messages | |
| Send attachments | (API limitation) |
| Archive and unarchive | |
### Comments
| Feature | Supported |
|---------|-----------|
| List comments on posts | |
| Reply to comments | |
| Delete comments | |
| Upvote and downvote | |
| Remove vote | |
| Edit comments | |
When replying to comments, you must provide the `subreddit` parameter. `PATCH /v1/inbox/comments/{postId}/{commentId}` edits the body of a comment the account posted, which Reddit is the only platform to allow, and the comment id is unchanged. Votes go through [Vote on a Reddit post or comment](/connect/vote-reddit-thing) with `thingId` (`t3_` for a post, `t1_` for a comment) and `direction` (`1`, `-1` or `0` to clear). Reddit's terms require every vote to be a human's own action proxied one to one; automated or agent-decided voting is vote manipulation and puts API access at risk.
[Messages](/messages/list-inbox-conversations) and [Comments](/comments/list-inbox-comments) document the inbox endpoints.
## What you cannot do
Reddit's API does not expose:
- Polls
- Crossposts to other subreddits
- Editing a post title after creation
- Collections
- Live chat threads
- Awards
- Separate upvote and downvote counts (score only)
## Common errors
| Error | Cause | Fix |
|-------|-------|-----|
| "SUBREDDIT_NOTALLOWED: only trusted members" | The subreddit restricts who can post | Join the community, build karma, or choose another subreddit. |
| "NO_SELFS: doesn't allow text posts" | The subreddit accepts link or image posts only | Set `url` or attach an image. |
| "SUBMIT_VALIDATION_FLAIR_REQUIRED" | The subreddit requires a flair on every post | Fetch the flairs with [List subreddit flairs](/connect/get-reddit-flairs) and pass the right `flairId`. |
| "SUBREDDIT_NOEXIST" | A typo in the subreddit name, or the subreddit is private | Check the spelling and drop the `r/` prefix. |
| "AI-generated content not allowed" | The subreddit bans AI-generated content | Write original content or choose another subreddit. |
| "Reddit removed the post" | Moderators or AutoMod removed the post after submission | Read the subreddit rules and make the content comply. |
| "Reddit requires a subreddit" | No `subreddit` in the request and no default on the account | Set `platformSpecificData.subreddit`. |
| "Reddit rate limit reached... Retry in Ns." or "Quota resets in Ns" (`429`) | Reddit rate-limits per OAuth application, and the budget is shared across every Zernio customer, so a `429` can arrive at low request volume on your side | Wait the number of seconds in the message, then retry. New accounts are limited to around 10 posts per day. See [Reddit rate limits](/guides/rate-limits#reddits-budget-is-shared). |
A `publishNow: true` post that Reddit rejects returns `207` with `post.status: "failed"` and the message in `platforms[].errorMessage`:
```json
{
"message": "Post created but publishing failed",
"error": "All platforms failed",
"post": {
"_id": "65f1c0a9e2b5af0012ab34cd",
"status": "failed",
"platforms": [
{
"platform": "reddit",
"status": "failed",
"errorMessage": "SUBMIT_VALIDATION_FLAIR_REQUIRED"
}
]
}
}
```
Fetch the subreddit's flairs, add `flairId` to `platformSpecificData` and create the post again. `207` is a 2xx status, so `fetch(...).ok` is `true`; branch on the status code and on `post.status`. [Error handling](/guides/error-handling) covers the envelope.
## Related
- [Connecting accounts](/guides/connecting-accounts): the OAuth flow.
- [Create post](/posts/create-post): every field of the request.
- [Reddit search](/reddit-search/search-reddit): search posts and browse subreddit feeds.
- [Get subreddit rules](/connect/get-subreddit-rules) and [List Reddit subreddits](/connect/get-reddit-subreddits): check a subreddit before posting.
- [Messages](/messages/list-inbox-conversations) and [Comments](/comments/list-inbox-comments): the inbox API.
---
# Shopify
Connect a Shopify store and create, schedule, update and delete its blog articles through the Blogs API; a store publishes no social posts.
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
Create, schedule, update and delete the blog articles of a Shopify store with the [Blogs API](/blogs/list-blogs) and a connected account with `platform: "shopify"`. Shopify is connect-only: a store never appears as a target in `POST /v1/posts`, publishes no social posts and reports no analytics.
## Quick reference
| Property | Value |
|----------|-------|
| Platform value | `shopify` |
| What it manages | Storefront blogs and blog articles |
| Auth | OAuth 2.0 (store domain required) or a custom-app Admin token |
| Scopes | `read_content`, `write_content` |
| Social posting | No |
| Analytics | No |
| Media requirements | None (article images are referenced by URL) |
| Scheduling | Yes, native (Shopify publishes at `publishDate`; no Zernio queue) |
| Drafts | Yes (`isPublished: false`) |
| Article body | HTML (`bodyHtml`) |
| SEO fields | Yes (`seo.title`, `seo.description`) |
| Pagination | Cursor (`limit` 1 to 50, default 20, plus `nextCursor`) |
## Before you start
Shopify requires the store's `myshopify.com` domain before OAuth can start. Shopify has no store picker and no lookup from a merchant to their shops, so an authorization URL can only be built for a domain you already know; collect it from the merchant first. A merchant who installs from the Shopify App Store never types it, because Shopify supplies the domain to Zernio on that path.
Blog and article ids are Shopify's own numeric ids, not Zernio object ids. Read them from API responses; never construct them.
## Connect
Call `GET /v1/connect/shopify` with `profileId` and `shop` on [Get Shopify OAuth connect URL](/connect/get-shopify-connect-url). Send the merchant to the returned `authUrl`; after they approve the install, Shopify calls Zernio's callback, the account is created on your profile, and the browser lands on your `redirect_url`. The [connecting accounts guide](/guides/connecting-accounts#shopify) covers the flow and [scopes](/guides/connecting-accounts#scopes) in general.
```typescript
import Zernio from '@zernio/node';
const zernio = new Zernio();
const { data: connect } = await zernio.connect.getShopifyConnectUrl({
query: {
profileId: '66a1f0c2a4b9d3e8f1a2b3c4',
shop: 'your-store.myshopify.com',
redirect_url: 'https://myapp.com/connected'
}
});
// Send the merchant's browser to connect.authUrl
```
```python
from zernio import Zernio
client = Zernio()
connect = client.connect.get_shopify_connect_url(
profile_id="66a1f0c2a4b9d3e8f1a2b3c4",
shop="your-store.myshopify.com",
redirect_url="https://myapp.com/connected"
)
# Send the merchant's browser to connect["authUrl"]
```
```bash
curl "https://zernio.com/api/v1/connect/shopify?profileId=66a1f0c2a4b9d3e8f1a2b3c4&shop=your-store.myshopify.com&redirect_url=https://myapp.com/connected" \
-H "Authorization: Bearer $ZERNIO_API_KEY"
```
Response (`200`):
```json
{
"authUrl": "https://your-store.myshopify.com/admin/oauth/authorize?client_id=...",
"state": "..."
}
```
`shop` accepts the full domain or the bare `your-store` prefix. `redirect_url` must be an absolute http(s) URL or an app scheme such as `myapp://callback`; a relative path is rejected with `400 INVALID_REDIRECT_URL`. Connecting the same profile to the same store again refreshes the stored token in place instead of creating a second account.
### Custom-app Admin token
To skip the browser flow, the merchant creates a custom app in their Shopify admin (Settings, then Apps and sales channels, then Develop apps) with the `read_content` and `write_content` scopes and hands you its Admin API access token, which starts with `shpat_`. Exchange it with [Connect a Shopify store with a custom-app Admin token](/connect/connect-shopify-with-token):
```bash
curl -X POST "https://zernio.com/api/v1/connect/shopify/token" \
-H "Authorization: Bearer $ZERNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"profileId": "66a1f0c2a4b9d3e8f1a2b3c4",
"shop": "your-store.myshopify.com",
"accessToken": "shpat_..."
}'
```
Response (`200`):
```json
{
"account": {
"_id": "66b2e19d8c3f5a7e9d0b1c2d",
"platform": "shopify",
"username": "your-store.myshopify.com",
"displayName": "Your Store",
"profileId": "66a1f0c2a4b9d3e8f1a2b3c4"
}
}
```
`shop` is required here too: an Admin token does not identify its own store. Zernio validates the token against the store before saving anything, and custom-app tokens do not expire.
### OAuth scopes
| Scope | What it enables |
|-------|-----------------|
| `read_content` | Read the store's blogs and articles |
| `write_content` | Create, update and delete blogs and articles |
Content scopes only. Zernio requests no access to customers, orders, products or payments.
### When the merchant uninstalls
Shopify invalidates its own token the moment the app is uninstalled and sends Zernio the `app/uninstalled` webhook, so there is nothing to revoke and nothing to call. Zernio deactivates that store's account: it stops appearing in `GET /v1/accounts` as active, and any request naming its `accountId` fails. Scheduled articles on the account are held through the disconnect grace period rather than deleted at once, so a reinstall inside that window brings them back with the account. Reinstalling runs the same [connect](#connect) flow and issues a fresh token.
## Publish
There are no social posts. A connected store publishes blog articles: a store has one or more blogs (Shopify creates a "News" blog by default), and each blog holds articles. All content lives on Shopify; Zernio proxies it and stores nothing.
### List blogs
Start by listing the blogs to get the `id` you will write into ([List blogs](/blogs/list-blogs)):
```bash
curl "https://zernio.com/api/v1/accounts/66b2e19d8c3f5a7e9d0b1c2d/blogs" \
-H "Authorization: Bearer $ZERNIO_API_KEY"
```
Response (`200`):
```json
{
"platform": "shopify",
"blogs": [
{ "id": "121793282419", "platform": "shopify", "title": "News", "handle": "news" }
],
"nextCursor": null
}
```
### Create an article
Call `POST /v1/accounts/{accountId}/blogs/{blogId}/articles` with `title` and `bodyHtml` ([Create an article](/blogs/create-blog-article)).
```typescript
const { data: created } = await zernio.blogs.createBlogArticle({
path: { accountId: '66b2e19d8c3f5a7e9d0b1c2d', blogId: '121793282419' },
body: {
title: 'Autumn collection preview',
bodyHtml: 'The first pieces land next month.
',
tags: ['autumn', 'new-arrivals'],
author: 'Maria Costa',
excerpt: 'An early look at what is arriving this September.',
isPublished: true
}
});
console.log(created.article.id);
```
```python
created = client.blogs.create_blog_article(
account_id="66b2e19d8c3f5a7e9d0b1c2d",
blog_id="121793282419",
title="Autumn collection preview",
body_html="The first pieces land next month.
",
tags=["autumn", "new-arrivals"],
author="Maria Costa",
excerpt="An early look at what is arriving this September.",
is_published=True
)
print(created["article"]["id"])
```
```bash
curl -X POST "https://zernio.com/api/v1/accounts/66b2e19d8c3f5a7e9d0b1c2d/blogs/121793282419/articles" \
-H "Authorization: Bearer $ZERNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Autumn collection preview",
"bodyHtml": "The first pieces land next month.
",
"tags": ["autumn", "new-arrivals"],
"author": "Maria Costa",
"excerpt": "An early look at what is arriving this September.",
"isPublished": true
}'
```
Response (`201`):
```json
{
"platform": "shopify",
"article": {
"id": "589234567890",
"blogId": "121793282419",
"platform": "shopify",
"title": "Autumn collection preview",
"handle": "autumn-collection-preview",
"tags": ["autumn", "new-arrivals"],
"isPublished": true,
"publishedAt": "2026-09-08T09:00:00Z"
}
}
```
### Draft
`isPublished: false` keeps the article as a draft; it reads back with `publishedAt: null`:
```json
{
"title": "Autumn collection preview",
"bodyHtml": "The first pieces land next month.
",
"isPublished": false
}
```
### Scheduled article
A future `publishDate` schedules the article natively on Shopify. Shopify publishes it at that time with no Zernio queue involved, and until then the article reads back as `isPublished: false` with `publishedAt` set to the future date:
```json
{
"title": "Autumn collection preview",
"bodyHtml": "The first pieces land next month.
",
"publishDate": "2027-01-01T12:00:00+01:00"
}
```
### Featured image and SEO
`image.url` sets the featured image; Shopify downloads it, so the URL must be publicly reachable. `seo.title` and `seo.description` map to Shopify's `title_tag` and `description_tag` metafields, which themes read for the page title and meta description:
```json
{
"title": "Autumn collection preview",
"bodyHtml": "The first pieces land next month.
",
"image": { "url": "https://cdn.example.com/autumn.jpg", "altText": "Wool coats on a rail" },
"seo": { "title": "Autumn collection preview", "description": "An early look at the autumn pieces." }
}
```
### Every operation
| Operation | Endpoint |
|-----------|----------|
| [List blogs](/blogs/list-blogs) | `GET /v1/accounts/{accountId}/blogs` |
| [Create a blog](/blogs/create-blog) | `POST /v1/accounts/{accountId}/blogs` |
| [Get a blog](/blogs/get-blog) | `GET /v1/accounts/{accountId}/blogs/{blogId}` |
| [Update a blog](/blogs/update-blog) | `PATCH /v1/accounts/{accountId}/blogs/{blogId}` |
| [Delete a blog](/blogs/delete-blog) | `DELETE /v1/accounts/{accountId}/blogs/{blogId}` |
| [List articles](/blogs/list-blog-articles) | `GET /v1/accounts/{accountId}/blogs/{blogId}/articles` |
| [Create an article](/blogs/create-blog-article) | `POST /v1/accounts/{accountId}/blogs/{blogId}/articles` |
| [Get an article](/blogs/get-blog-article) | `GET /v1/accounts/{accountId}/blogs/{blogId}/articles/{articleId}` |
| [Update an article](/blogs/update-blog-article) | `PATCH /v1/accounts/{accountId}/blogs/{blogId}/articles/{articleId}` |
| [Delete an article](/blogs/delete-blog-article) | `DELETE /v1/accounts/{accountId}/blogs/{blogId}/articles/{articleId}` |
## Platform fields
There is no `platformSpecificData` for Shopify, because a store is not a `POST /v1/posts` target. The article fields:
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `title` | string | | Required. |
| `bodyHtml` | string | | Article body as HTML. |
| `handle` | string | (slug of the title) | URL slug. Shopify sets it once, at creation; renaming the article later leaves the URL unchanged, so set it explicitly when the URL matters. |
| `tags` | Array\ | | Shopify returns them alphabetized, not in the order you sent. |
| `author` | string | | Display name of the author. |
| `excerpt` | string | | Short summary shown in blog listings. |
| `image` | \{url, altText?\} | | Featured image, downloaded by Shopify from a public URL. |
| `seo` | \{title?, description?\} | | Maps to Shopify's `title_tag` and `description_tag` metafields. |
| `isPublished` | boolean | | `false` creates a draft. |
| `publishDate` | datetime | | ISO 8601 with offset or `Z`. A future date schedules publication on Shopify. |
## Media requirements
None. Article images are referenced by URL in `image.url` and downloaded by Shopify, not uploaded to Zernio.
## Analytics
Shopify exposes no analytics through Zernio.
## Inbox
Shopify has no inbox.
## What you cannot do
Shopify's connection does not expose:
- Social posts (`POST /v1/posts` rejects a Shopify account)
- Analytics
- An inbox
- Media uploads
- Products, orders and customers (the granted scopes do not permit them)
- Restoring a deleted blog or article (deletes are permanent, and deleting a blog deletes every article inside it)
## Common errors
| Error | Cause | Fix |
|-------|-------|-----|
| `400 INVALID_REDIRECT_URL` | `redirect_url` is a relative path | Pass an absolute http(s) URL or an app scheme. |
| `400` on `GET /v1/connect/shopify` | `shop` is not a `myshopify.com` store domain | Pass `your-store.myshopify.com` or the bare `your-store` prefix. |
| `400` on a Blogs endpoint | `blogId` is not numeric, or the account is on a platform without blogs | Read the id from [List blogs](/blogs/list-blogs); use a Shopify account. |
| `403 insufficient_permissions` | Shopify rejected the request | Reconnect the store to restore access. |
| `404 blog_article_not_found` | The article was deleted, or the id belongs to another blog | Deletes are permanent; list the blog's articles to find a current id. |
| `405` | The platform lacks this specific Blogs operation | Not available for this account. |
| `429` | Rate limited by Zernio or by Shopify | Retry later. See [rate limits](/guides/rate-limits). |
Deleting an article returns `204`, and a later read of it returns `404`:
```json
{
"error": "Article not found",
"type": "not_found",
"code": "blog_article_not_found"
}
```
Zernio stores nothing to restore it from. [Error handling](/guides/error-handling) covers the envelope.
## Related
- [Connecting accounts](/guides/connecting-accounts#shopify): the OAuth and Admin-token paths.
- [Blogs API](/blogs/list-blogs): every blog and article endpoint.
- [Get Shopify OAuth connect URL](/connect/get-shopify-connect-url): every parameter of the connect call.
- [Connect with an Admin token](/connect/connect-shopify-with-token): the token-paste alternative.
- [Platforms overview](/platforms): the 16 posting platforms.
---
# Slack
Publish messages and thread replies to a Slack channel with the Zernio API, with file uploads, a per-message bot identity, and an inbox that receives DMs and mentions in realtime.
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
Publish messages and thread replies to a Slack channel with `POST /v1/posts` and `platform: "slack"`. The same account receives DMs and bot mentions in the inbox, and can start a DM with any member of its workspace.
## Quick reference
| Property | Value |
|----------|-------|
| Content limit | 40,000 characters (Slack truncates silently beyond this) |
| Edit limit | 4,000 characters (longer posts cannot be edited) |
| Media per post | Up to 10 files |
| Formatting | Slack `mrkdwn` |
| Post types | Channel messages, thread replies |
| Scheduling | Yes (Zernio-side; Slack's native scheduler is not used) |
| Threads | Yes (`threadTs`) |
| Link unfurling | Configurable per post |
| Per-message identity | Yes (display name and avatar override) |
| Inbox (DMs) | Yes |
| Start a conversation | Yes (DM any member of the workspace) |
| Inbox (channel messages) | Mentions and threads only, not all chatter |
| Contacts and built-in automations | DMs only |
| Analytics | No (Slack has no public message analytics API) |
## Before you start
Slack requires the Zernio bot app installed in the workspace. You do not create your own Slack app, host a bot or manage tokens: the user installs Zernio's app through OAuth and you post through the API.
One connected account is one channel. To publish to several channels, connect each one as its own account. Public channels are joined automatically on connect. A private channel needs a member to run `/invite @Zernio` in it first, because the bot cannot join a private channel on its own, and until then it does not appear in the channel list.
Some workspaces require admin approval for new apps, so Slack may show a "request approval" screen, and a workspace can be configured to allow only apps listed in the Slack Marketplace. Zernio's app is distributed but not yet listed, so Slack says it "has not been reviewed by Slack" during install. That is expected.
## Connect
Call `GET /v1/connect/slack` with `profileId` on [List Slack channels](/connect/list-slack-channels). With no `pendingDataToken` or `accountId` it returns the `authUrl` and `state` that start OAuth, like `GET /v1/connect/{platform}` does for other platforms. Slack installs the bot into a workspace, not a channel, so the flow ends with a channel choice:
1. Send the user to `authUrl`; they authorize the workspace on Slack's consent screen.
2. Zernio lists the workspace's channels and the user picks one.
3. `POST /v1/connect/slack` saves the channel as an account.
In headless mode (`headless=true`) the post-OAuth redirect carries a `pendingDataToken` and `connect_token`. List the channels yourself:
```bash
curl "https://zernio.com/api/v1/connect/slack?profileId=66a1f0c2a4b9d3e8f1a2b3c4&pendingDataToken=$PENDING_DATA_TOKEN" \
-H "Authorization: Bearer $ZERNIO_API_KEY"
```
Response (`200`):
```json
{
"team": { "id": "T0123456789", "name": "Example Co", "icon": "https://..." },
"channels": [
{ "id": "C0123456789", "name": "marketing", "isPrivate": false, "isMember": true },
{ "id": "G0123456789", "name": "launch-room", "isPrivate": true, "isMember": false }
]
}
```
Channels are read live from Slack, public and private, archived excluded, up to 2,000. `isMember` says whether the bot is already in the channel. Then finalize with [Connect Slack channel](/connect/connect-slack-channel):
```bash
curl -X POST https://zernio.com/api/v1/connect/slack \
-H "Authorization: Bearer $ZERNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"profileId": "66a1f0c2a4b9d3e8f1a2b3c4",
"channelId": "C0123456789",
"pendingDataToken": "'"$PENDING_DATA_TOKEN"'"
}'
```
Response (`200`):
```json
{
"message": "Slack channel connected successfully",
"account": {
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"platform": "slack",
"username": "marketing",
"displayName": "marketing",
"isActive": true,
"channelId": "C0123456789",
"teamId": "T0123456789",
"teamName": "Example Co"
}
}
```
`account.accountId` is the id every sample below uses. The channel is now an account in that profile, so it also shows up in `GET /v1/accounts?profileId=66a1f0c2a4b9d3e8f1a2b3c4`.
Adding another channel from the same workspace needs no second OAuth: call `GET /v1/connect/slack` with `profileId` and `accountId` of an existing Slack account to list the channels, then `POST` with `accountId` instead of `pendingDataToken`. The [connecting accounts guide](/guides/connecting-accounts) covers the flow and [scopes](/guides/connecting-accounts#scopes) in general.
### OAuth scopes
Publishing and the inbox run through a single bot installation:
| Scope | What it enables |
|-------|-----------------|
| `chat:write` | Post messages as the bot |
| `chat:write.public` | Post to public channels the bot has not joined |
| `chat:write.customize` | Per-message display name and avatar override |
| `channels:join` | Join public channels on connect |
| `files:write` | Upload media with a post |
| `channels:read`, `groups:read` | List channels so you can pick one |
| `team:read` | Read workspace name and icon |
| `channels:history`, `groups:history`, `im:history`, `mpim:history` | Receive inbound messages for the inbox |
| `im:read`, `mpim:read`, `im:write` | Direct message conversations |
| `users:read` | Resolve sender names on inbound messages |
| `files:read` | Inbound message attachments |
Workspaces connected before the inbox shipped must reconnect once. Existing bot tokens keep the scopes they were granted, so inbound messages fail with `missing_scope` until the workspace re-authorizes from the dashboard.
## Publish
A plain post becomes a message in the channel the account was connected to. Fields in `platformSpecificData` on the Slack entry turn it into a thread reply, control unfurling and override the bot's identity.
### Channel message
```typescript
import Zernio from '@zernio/node';
const zernio = new Zernio();
const { data: published } = await zernio.posts.createPost({
body: {
content: 'Deploy finished :rocket:',
platforms: [
{ platform: 'slack', accountId: '66b2e19d8c3f5a7e9d0b1c2d' }
],
publishNow: true
}
});
console.log(published.post.platforms[0].platformPostUrl);
```
```python
from zernio import Zernio
client = Zernio()
published = client.posts.create_post(
content="Deploy finished :rocket:",
platforms=[
{"platform": "slack", "accountId": "66b2e19d8c3f5a7e9d0b1c2d"}
],
publish_now=True
)
print(published["post"]["platforms"][0]["platformPostUrl"])
```
```bash
curl -X POST https://zernio.com/api/v1/posts \
-H "Authorization: Bearer $ZERNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "Deploy finished :rocket:",
"platforms": [
{"platform": "slack", "accountId": "66b2e19d8c3f5a7e9d0b1c2d"}
],
"publishNow": true
}'
```
Response (`201`):
```json
{
"post": {
"_id": "65f1c0a9e2b5af0012ab34cd",
"status": "published",
"platforms": [
{
"platform": "slack",
"status": "published",
"platformPostUrl": "https://example-co.slack.com/archives/C0123456789/p1785320613863000"
}
]
}
}
```
Every sample below changes only the `mediaItems` or the `platforms` entry of this request.
There is no `channelId` field on a post. The channel is whichever one the account was connected to; passing `channelId` in `platformSpecificData` returns a `400`. Connect the channel you want and target its `accountId`.
### Thread reply
`threadTs` posts the message inside an existing thread. Pass the parent message's timestamp:
```json
{
"platform": "slack",
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"platformSpecificData": { "threadTs": "1785320613.863000" }
}
```
### Files
Up to 10 files per post, uploaded through Slack's file API with the text as the caption:
```json
"mediaItems": [
{ "type": "image", "url": "https://cdn.example.com/dashboard.png" },
{ "type": "image", "url": "https://cdn.example.com/error-rate.png" }
]
```
### Message identity
`username` and `iconUrl` override the bot's display name and avatar for one message. The override is cosmetic: Slack still shows an APP badge, and the app's identity in the sidebar stays "Zernio". Set a default identity for every post from the channel with [Slack account settings](/accounts/get-slack-settings); the per-post fields still win.
```json
{
"platform": "slack",
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"platformSpecificData": {
"unfurlLinks": false,
"username": "Status Bot",
"iconUrl": "https://example.com/status.png"
}
}
```
### Edit and delete
[Edit post](/posts/edit-post) replaces the message text up to 4,000 characters; media cannot be swapped. Longer messages cannot be edited, because Slack's edit limit is stricter than its posting limit. There is no time window unless workspace admins restrict message editing. Unpublishing deletes the Slack message; workspace retention or compliance settings can block the deletion, which is an admin setting, not a Zernio error. A media post occasionally cannot be edited or deleted when Slack returned no message reference at publish time.
## Platform fields
All fields go in `platformSpecificData` on the Slack entry.
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `threadTs` | string | | Post as a reply inside an existing thread. The parent message's timestamp, for example `"1503435956.000247"`. |
| `unfurlLinks` | boolean | `true` | Expand links in the text into preview cards. |
| `unfurlMedia` | boolean | `true` | Expand media links into inline previews. |
| `username` | string | (account default) | Override the bot's display name for this message. |
| `iconUrl` | string | (account default) | Override the bot's avatar for this message. |
## Media requirements
Up to 10 files per post, uploaded through Slack's file API with the text as the caption. Slack is the one platform Zernio neither compresses nor transcodes for, and it applies no format allowlist here: the bytes behind your URL reach Slack as they are, whatever the type. The per-file ceiling is therefore Slack's own, and the workspace's upload policy and, on Slack's free plan, its storage cap decide whether an upload succeeds; a file Slack refuses fails the post with Slack's message. Zernio names the upload from the media item, `type: "video"` as `.mp4` and `type: "gif"` as `.gif`, and otherwise from `filename`. Host files on a public URL or through the [media endpoint](/guides/media-uploads), which caps a single file at 5 GB.
## Analytics
Slack has no public message analytics API, so `GET /v1/analytics?platform=slack` reports zero for every Slack post. Reactions and thread replies reach you through the [inbox](#inbox) instead.
## Inbox
Slack supports DMs and channel mentions in the [inbox](/messages/list-inbox-conversations), delivered in realtime through Slack's Events API. Zernio ingests:
- DMs to the Zernio bot
- Channel messages that @-mention the bot
- Replies in threads the bot is already part of, including under a post Zernio published
- The bot's own messages
- Emoji reactions added to or removed from a message, as [`reaction.received`](/webhooks/inbox#reactionreceived)
Ordinary channel conversation is deliberately not ingested. The inbox is not a mirror of the workspace, so day-to-day chatter in a connected channel is never stored.
Every incoming message fires the `message.received` event. Its `metadata` carries:
| Field | Meaning |
|-------|---------|
| `threadTs` | Present when the message is a thread reply |
| `mentionsBot` | `true` when the message @-mentions the Zernio bot |
That pair covers the usual agent rule: answer when mentioned in a channel, always answer DMs, and reply inside the thread. Reply from the inbox or with [Send inbox message](/messages/send-inbox-message) on `POST /v1/inbox/conversations/{conversationId}/messages`; the message goes out as the bot, and `replyTo` threads the reply. `POST /v1/inbox/conversations/{conversationId}/messages/{messageId}/reactions` reacts to a message; the emoji has to have a Slack name, such as `:thumbsup:`, and an unnamed character returns a `400`.
Constraints:
- No history backfill. The inbox starts at connect time, a Slack restriction for apps not listed in the Marketplace.
- Messages from other bots appear for context but never trigger automations or webhook events (loop guard).
- Attachments appear as links that open in Slack.
- Contacts and the built-in automations (sequences, workflows) are DM-only. Someone who DMs the bot becomes a contact; people talking in a connected channel do not, otherwise every colleague in the channel would be added as a contact. Automate channels with your own agent reacting to `message.received`.
### Start a conversation
You do not have to wait for someone to message the bot first. Look up a workspace member with [List Slack members](/slack/list-slack-members), then open the DM by passing their member id as `participantId` to [Create inbox conversation](/messages/create-inbox-conversation):
```typescript
const { data: members } = await zernio.slack.listSlackMembers({
path: { accountId: '66b2e19d8c3f5a7e9d0b1c2d' },
query: { query: 'maria' }
});
const { data: conversation } = await zernio.messages.createInboxConversation({
body: {
accountId: '66b2e19d8c3f5a7e9d0b1c2d',
participantId: members.members[0].id,
message: 'Hi Maria, the campaign is live.'
}
});
console.log(conversation.data.conversationId);
```
```python
members = client.slack.list_slack_members(
account_id="66b2e19d8c3f5a7e9d0b1c2d",
query="maria"
)
conversation = client.messages.create_inbox_conversation(
account_id="66b2e19d8c3f5a7e9d0b1c2d",
participant_id=members["members"][0]["id"],
message="Hi Maria, the campaign is live."
)
print(conversation["data"]["conversationId"])
```
```bash
curl "https://zernio.com/api/v1/accounts/66b2e19d8c3f5a7e9d0b1c2d/slack-members?query=maria" \
-H "Authorization: Bearer $ZERNIO_API_KEY"
curl -X POST https://zernio.com/api/v1/inbox/conversations \
-H "Authorization: Bearer $ZERNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"participantId": "U01ABCDEF",
"message": "Hi Maria, the campaign is live."
}'
```
Response (`200`) from the member lookup:
```json
{
"members": [
{ "id": "U01ABCDEF", "name": "Maria Lopez", "username": "maria", "picture": "https://..." }
]
}
```
Response (`201`) from the conversation call:
```json
{
"success": true,
"data": {
"messageId": "1785320613.863000",
"conversationId": "66c3d2ae7b4f6c8d0e1f2a3b",
"participantId": "U01ABCDEF",
"participantName": "Maria Lopez"
}
}
```
Slack addresses people by opaque id, never by name. Bots, deactivated members and Slackbot are left out of the lookup because they cannot hold a DM conversation. The DM comes from the workspace's connected bot, so it reaches members of that workspace only, and the reply lands in the same conversation.
## What you cannot do
Slack's bot API does not expose:
- Messaging people outside the connected workspace
- Posting to several channels from one connected account (connect each channel separately)
- Choosing the channel per post (`channelId` in `platformSpecificData` returns a `400`)
- Engagement metrics
- Message history from before the connection
- Joining a private channel without being invited
## Common errors
| Error | Cause | Fix |
|-------|-------|-----|
| Bot is not a member of this channel | The bot was removed, or the channel is private | Run `/invite @Zernio` in the channel. |
| Channel was not found or the bot lost access | The channel was deleted or the bot lost visibility | Reconnect, or connect another channel. |
| A workspace setting prevents posting | A Slack admin restricted posting in that channel | Only a Slack admin can lift it. |
| File uploads are disabled or too large | The workspace's upload policy or its free-plan storage cap | Check the workspace's upload policy and storage. |
| Workspace access was revoked | The app was uninstalled from Slack | Reconnect from the dashboard. |
| `missing_scope` on inbound messages | The workspace was connected before the inbox shipped | Reconnect once. |
A `publishNow: true` post that Slack rejects returns `207` with `post.status: "failed"` and the message in `platforms[].errorMessage`:
```json
{
"message": "Post created but publishing failed",
"error": "All platforms failed",
"post": {
"_id": "65f1c0a9e2b5af0012ab34cd",
"status": "failed",
"platforms": [
{
"platform": "slack",
"status": "failed",
"errorMessage": "Bot is not a member of this channel"
}
]
}
}
```
`207` is a 2xx status, so `fetch(...).ok` is `true`; branch on the status code and on `post.status`. [Error handling](/guides/error-handling) covers the envelope.
Disconnecting one channel account does not revoke the workspace token, so sibling channel accounts keep working. Uninstalling the Zernio app from Slack disconnects every channel account of that workspace in Zernio automatically.
## Related
- [Connecting accounts](/guides/connecting-accounts): the OAuth flow in general.
- [Create post](/posts/create-post) and [Edit post](/posts/edit-post): every field of the request.
- [Slack account settings](/accounts/get-slack-settings): the default message identity for a channel.
- [Messages](/messages/list-inbox-conversations): the inbox API.
- [Account health](/accounts/get-all-accounts-health): verify the connection.
- [Pricing](/pricing): what the inbox and outbound messages cost.
---
# Snapchat
Publish Stories, Saved Stories and Spotlight videos to a Snapchat Public Profile with the Zernio API, once your account is approved for the closed beta.
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
Publish Stories, Saved Stories and Spotlight videos to Snapchat with `POST /v1/posts` and `platform: "snapchat"`. Snapchat is in closed beta, so new connections need approval before anything on this page works.
## Quick reference
| Property | Value |
|----------|-------|
| Title limit | 45 characters (Saved Stories) |
| Description limit | 160 characters (Spotlight, including hashtags) |
| Media per post | 1 (a single image or video) |
| Image formats | JPEG, PNG |
| Image max size | 20 MB |
| Video format | MP4 only |
| Video max size | 500 MB |
| Video duration | 5 to 60 seconds |
| Post types | Story, Saved Story, Spotlight |
| Scheduling | Yes |
| Inbox | No |
| Analytics | Yes (views, unique viewers, shares) |
## Before you start
Snapchat requires a Public Profile (Person, Business or Official); a regular Snapchat account cannot publish through the API. Every post carries exactly 1 image or video: there are no text-only posts, no carousels and no albums, and 9:16 vertical media is expected. Zernio encrypts each file with AES-256-CBC before uploading it to Snapchat, so nothing changes on your side.
Snapchat is in closed beta. `GET /v1/connect/snapchat` returns `403` with code `PLATFORM_BETA_RESTRICTED` for any account that is not on the beta allowlist, and there is no public release date yet. Everything on this page applies once your account is approved.
## Connect
Call `GET /v1/connect/snapchat` with `profileId` on [Get OAuth connect URL](/connect/get-connect-url). After the user authorizes, they pick which Public Profile to connect, so Snapchat is one of the [platforms requiring secondary selection](/guides/connecting-accounts#platforms-requiring-secondary-selection): in standard mode Zernio hosts that screen, in headless mode you build it. The [connecting accounts guide](/guides/connecting-accounts) covers the OAuth flow and [scopes](/guides/connecting-accounts#scopes) in general.
```typescript
import Zernio from '@zernio/node';
const zernio = new Zernio();
const { data: connect } = await zernio.connect.getConnectUrl({
path: { platform: 'snapchat' },
query: { profileId: '66a1f0c2a4b9d3e8f1a2b3c4', redirect_url: 'https://myapp.com/callback' }
});
// Send the user's browser to connect.authUrl
```
```python
from zernio import Zernio
client = Zernio()
connect = client.connect.get_connect_url(
platform="snapchat",
profile_id="66a1f0c2a4b9d3e8f1a2b3c4",
redirect_url="https://myapp.com/callback"
)
# Send the user's browser to connect["authUrl"]
```
```bash
curl "https://zernio.com/api/v1/connect/snapchat?profileId=66a1f0c2a4b9d3e8f1a2b3c4&redirect_url=https://myapp.com/callback" \
-H "Authorization: Bearer $ZERNIO_API_KEY"
```
Response (`200`):
```json
{
"authUrl": "https://accounts.snapchat.com/accounts/oauth2/auth?client_id=...",
"state": "..."
}
```
In standard mode the user picks a Public Profile on Zernio's screen and lands on your `redirect_url` with the connection details appended.
### OAuth scopes
The consent screen asks for 1 scope:
| Scope | What it enables |
|-------|-----------------|
| `snapchat-profile-api` | Manage the connected Public Profile: publish to Spotlight and Stories, read profile data and analytics |
### Headless mode
Add `headless=true` to the connect call to show your own Public Profile picker. After OAuth the user lands on your `redirect_url` with these query parameters:
- `tempToken`: temporary Snapchat access token
- `userProfile`: URL-encoded JSON with the user's info
- `publicProfiles`: URL-encoded JSON array of the available Public Profiles
- `connect_token`: short-lived token that authenticates the 2 calls below
- `platform=snapchat` and `step=select_public_profile`
List the Public Profiles with [List Snapchat profiles](/connect/list-snapchat-profiles), passing `connect_token` in the `X-Connect-Token` header:
```typescript
const { data: profiles } = await zernio.connect.snapchat.listSnapchatProfiles({
headers: { 'X-Connect-Token': connectToken },
query: { profileId: '66a1f0c2a4b9d3e8f1a2b3c4', tempToken }
});
console.log(profiles.publicProfiles);
```
```python
profiles = client.connect.list_snapchat_profiles(
profile_id="66a1f0c2a4b9d3e8f1a2b3c4",
temp_token=temp_token,
x_connect_token=connect_token
)
print(profiles["publicProfiles"])
```
```bash
curl "https://zernio.com/api/v1/connect/snapchat/select-profile?profileId=66a1f0c2a4b9d3e8f1a2b3c4&tempToken=$TEMP_TOKEN" \
-H "X-Connect-Token: $CONNECT_TOKEN"
```
Response (`200`):
```json
{
"publicProfiles": [
{
"id": "abc123-def456",
"display_name": "My Brand",
"username": "mybrand",
"profile_image_url": "https://cf-st.sc-cdn.net/...",
"subscriber_count": 15000
},
{
"id": "xyz789-uvw012",
"display_name": "Side Project",
"username": "sideproject",
"profile_image_url": "https://cf-st.sc-cdn.net/...",
"subscriber_count": 5000
}
]
}
```
Connect the chosen profile with [Select Snapchat profile](/connect/select-snapchat-profile):
```typescript
const { data: selected } = await zernio.connect.snapchat.selectSnapchatProfile({
headers: { 'X-Connect-Token': connectToken },
body: {
profileId: '66a1f0c2a4b9d3e8f1a2b3c4',
selectedPublicProfile: {
id: 'abc123-def456',
display_name: 'My Brand',
username: 'mybrand'
},
tempToken,
userProfile
}
});
console.log(selected.account.accountId);
```
```python
selected = client.connect.select_snapchat_profile(
profile_id="66a1f0c2a4b9d3e8f1a2b3c4",
selected_public_profile={
"id": "abc123-def456",
"display_name": "My Brand",
"username": "mybrand"
},
temp_token=temp_token,
user_profile=user_profile,
x_connect_token=connect_token
)
print(selected["account"]["accountId"])
```
```bash
curl -X POST https://zernio.com/api/v1/connect/snapchat/select-profile \
-H "X-Connect-Token: $CONNECT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"profileId": "66a1f0c2a4b9d3e8f1a2b3c4",
"selectedPublicProfile": {
"id": "abc123-def456",
"display_name": "My Brand",
"username": "mybrand"
},
"tempToken": "'"$TEMP_TOKEN"'",
"userProfile": {
"id": "user123",
"username": "mybrand",
"displayName": "My Brand"
}
}'
```
Response (`200`):
```json
{
"message": "Snapchat connected successfully with public profile",
"account": {
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"platform": "snapchat",
"username": "mybrand",
"displayName": "My Brand",
"profilePicture": "https://cf-st.sc-cdn.net/...",
"isActive": true,
"publicProfileName": "My Brand"
}
}
```
`account.accountId` is the `accountId` for every call below.
## Publish
A plain post becomes a Story: `contentType` defaults to `"story"`. Set it to `"saved_story"` or `"spotlight"` for the other 2 types.
| `contentType` | What it publishes | Lifetime | Text |
|------|-------------|----------|--------------|
| `story` | A snap in the profile's Story | 24 hours | No caption |
| `saved_story` | A permanent story on the Public Profile | Permanent | `content` is the title, max 45 characters |
| `spotlight` | A video in Snapchat's Spotlight feed | Permanent | `content` is the description, max 160 characters, hashtags allowed |
### Story
A Story is visible for 24 hours and carries no caption, so `content` is not used:
```typescript
const { data: published } = await zernio.posts.createPost({
body: {
mediaItems: [
{ type: 'video', url: 'https://cdn.example.com/backstage.mp4' }
],
platforms: [
{
platform: 'snapchat',
accountId: '66b2e19d8c3f5a7e9d0b1c2d',
platformSpecificData: { contentType: 'story' }
}
],
publishNow: true
}
});
console.log(published.post.status);
```
```python
published = client.posts.create_post(
media_items=[
{"type": "video", "url": "https://cdn.example.com/backstage.mp4"}
],
platforms=[
{
"platform": "snapchat",
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"platformSpecificData": {"contentType": "story"}
}
],
publish_now=True
)
print(published["post"]["status"])
```
```bash
curl -X POST https://zernio.com/api/v1/posts \
-H "Authorization: Bearer $ZERNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"mediaItems": [
{"type": "video", "url": "https://cdn.example.com/backstage.mp4"}
],
"platforms": [
{
"platform": "snapchat",
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"platformSpecificData": {"contentType": "story"}
}
],
"publishNow": true
}'
```
Response (`201`):
```json
{
"post": {
"_id": "65f1c0a9e2b5af0012ab34cd",
"status": "published",
"platforms": [
{
"platform": "snapchat",
"status": "published"
}
]
}
}
```
Every sample below changes only the `content` or the `platforms` entry of this request. An image works the same way with `{ "type": "image", "url": "https://cdn.example.com/backstage.jpg" }`.
### Saved Story
A Saved Story stays on the Public Profile. `content` becomes its title, at most 45 characters:
```json
{
"content": "Behind the scenes",
"platforms": [
{
"platform": "snapchat",
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"platformSpecificData": { "contentType": "saved_story" }
}
]
}
```
### Spotlight
Spotlight is Snapchat's public video feed and takes video only. `content` becomes the description, at most 160 characters including hashtags:
```json
{
"content": "Sunset over the pier #sunset #nature",
"platforms": [
{
"platform": "snapchat",
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"platformSpecificData": { "contentType": "spotlight" }
}
]
}
```
## Platform fields
All fields go in `platformSpecificData` on the Snapchat entry.
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `contentType` | `"story"`, `"saved_story"`, `"spotlight"` | `"story"` | Where the media publishes. See [Publish](#publish). |
## Media requirements
Every post needs exactly 1 media item; a video outside these limits is rejected rather than compressed.
### Images
| Property | Requirement |
|----------|-------------|
| Formats | JPEG, PNG |
| Max file size | 20 MB |
| Recommended dimensions | 1080 x 1920 px |
| Aspect ratio | 9:16 (portrait) |
### Videos
| Property | Requirement |
|----------|-------------|
| Format | MP4 |
| Max file size | 500 MB |
| Duration | 5 to 60 seconds |
| Min resolution | 540 x 960 px |
| Recommended dimensions | 1080 x 1920 px |
| Aspect ratio | 9:16 (portrait) |
Zernio encrypts the file with AES-256-CBC before uploading it to Snapchat. Upload files through the [media endpoint](/guides/media-uploads) to get a URL that qualifies.
## Analytics
Call `GET /v1/analytics?platform=snapchat` ([Analytics API](/analytics/get-analytics)). Metrics are fetched per content type (`story`, `saved_story`, `spotlight`), and 3 of them come back: `views`, `reach`, which is Snapchat's unique viewer count, and `shares`. `likes`, `comments` and `saves` read 0, because Snapchat has none of them, and its screenshot count and completion rate stop at Snapchat rather than reaching this response.
```typescript
const { data: analytics } = await zernio.analytics.getAnalytics({
query: { platform: 'snapchat', fromDate: '2026-08-01', toDate: '2026-08-31' }
});
console.log(analytics.posts);
```
```python
analytics = client.analytics.get_analytics(
platform="snapchat",
from_date="2026-08-01",
to_date="2026-08-31"
)
print(analytics["posts"])
```
```bash
curl "https://zernio.com/api/v1/analytics?platform=snapchat&fromDate=2026-08-01&toDate=2026-08-31" \
-H "Authorization: Bearer $ZERNIO_API_KEY"
```
Response (`200`), one entry per post:
```json
{
"posts": [
{
"_id": "65f1c0a9e2b5af0012ab34cd",
"platform": "snapchat",
"status": "published",
"publishedAt": "2026-08-14T10:00:05Z",
"analytics": {
"views": 15420,
"reach": 12350,
"shares": 45
}
}
]
}
```
## Inbox
Snapchat has no inbox: its messaging API is closed to third-party apps, so there are no DMs, and snap comments are not accessible through the API.
## What you cannot do
Snapchat's API does not expose:
- AR lenses or filters
- Ads
- Snap Map
- Snapchat sounds
- Collaborative stories
- Friends' stories
- DMs or comments
- Text-only posts (media is required)
- More than 1 media item per post
## Common errors
| Error | Cause | Fix |
|-------|-------|-----|
| `403` with code `PLATFORM_BETA_RESTRICTED` | The account is not on the closed-beta allowlist | Ask for beta access; there is no public release date. |
| "Public Profile required" | The Snapchat account has no Public Profile | Create a Public Profile (Person, Business or Official) and select it during connection. |
| "Media is required" | The post has no media | Add an image or video. Snapchat has no text-only posts. |
| "Only one media item supported" | The post has more than 1 media item | Send a single image or video. |
| Video rejected | The video breaks a Snapchat requirement | Check duration (5 to 60 seconds), format (MP4 only), minimum resolution (540 x 960 px) and size (under 500 MB). |
| "Title too long" (Saved Stories) | `content` is over 45 characters | Shorten `content` to 45 characters or fewer. |
| "Description too long" (Spotlight) | `content` is over 160 characters | Shorten `content` to 160 characters or fewer, hashtags included. |
While the account is not approved, the connect call fails before any OAuth screen appears:
```json
{
"error": "Snapchat is in closed beta. New connections require approval.",
"type": "permission_error",
"code": "PLATFORM_BETA_RESTRICTED"
}
```
Branch on `code`, never on `error`. Request access for your account and retry the same call once it is approved; nothing else on the request needs to change. [Error handling](/guides/error-handling) covers the envelope.
## Related
- [Connecting accounts](/guides/connecting-accounts#platforms-requiring-secondary-selection): the OAuth flow and the Public Profile selection step.
- [Create post](/posts/create-post): every field of the request.
- [Media uploads](/guides/media-uploads): upload images and videos instead of hosting them.
- [Analytics](/analytics/get-analytics): post performance metrics.
- [Pricing](/pricing): what a connected account and analytics cost.
---
# Telegram
Publish text, photos, videos, documents and albums to Telegram channels and groups with the Zernio API, edit published messages, and run DMs and bot commands from the inbox.
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
Publish text, photos, videos, documents and albums to a Telegram channel or group with `POST /v1/posts` and `platform: "telegram"`. The same bot serves DMs and bot commands in the inbox.
## Quick reference
| Property | Value |
|----------|-------|
| Text limit | 4,096 characters (text messages) |
| Caption limit | 1,024 characters (media captions) |
| Images per album | 10 |
| Videos per album | 10 |
| Mixed media | Yes (images and videos in the same album) |
| Image formats | JPEG, PNG, GIF, WebP |
| Image max size | 10 MB (auto-compressed) |
| Video formats | MP4, MOV |
| Video max size | 50 MB (auto-compressed) |
| Scheduling | Yes |
| Editing published posts | Yes (text or caption, media cannot be changed) |
| Inbox (DMs) | Yes |
| Inbox (comments) | No |
| Analytics | No (Telegram limitation) |
## Before you start
Telegram requires Zernio's bot, `@ZernioScheduleBot`, to be an administrator of your channel or group with permission to post. There is no OAuth and there are no scopes: what the bot can do comes from the admin permissions you grant it. In a channel, posts appear as the channel's name and logo. In a group, posts appear as sent by "ZernioScheduleBot", and that cannot be changed.
Add `@ZernioScheduleBot` as an administrator with the Post Messages permission before you connect. A bot that is a plain member, or an admin without post permission, is the most common setup failure: the connect call returns `400` and every post fails with "Bot is not a member of the channel".
## Connect
Call `GET /v1/connect/telegram` with `profileId` to [generate an access code](/connect/get-telegram-connect-status), valid for 15 minutes. The user adds the bot to the channel or group and sends it the code; you poll until the connection completes. The [connecting accounts guide](/guides/connecting-accounts#telegram) covers where this fits next to the OAuth platforms.
### Access code flow
```typescript
import Zernio from '@zernio/node';
const zernio = new Zernio();
const { data: access } = await zernio.connect.telegram.getTelegramConnectStatus({
query: { profileId: '66a1f0c2a4b9d3e8f1a2b3c4' }
});
console.log(access.code, access.botUsername);
```
```python
from zernio import Zernio
client = Zernio()
access = client.connect.get_telegram_connect_status(
profile_id="66a1f0c2a4b9d3e8f1a2b3c4"
)
print(access["code"], access["botUsername"])
```
```bash
curl "https://zernio.com/api/v1/connect/telegram?profileId=66a1f0c2a4b9d3e8f1a2b3c4" \
-H "Authorization: Bearer $ZERNIO_API_KEY"
```
Response (`200`):
```json
{
"code": "ZRN-ABC123",
"expiresAt": "2027-01-01T12:15:00.000Z",
"expiresIn": 900,
"botUsername": "ZernioScheduleBot",
"instructions": [
"1. Add @ZernioScheduleBot as an administrator in your channel/group",
"2. Open a private chat with @ZernioScheduleBot",
"3. Send: ZRN-ABC123 @yourchannel (replace @yourchannel with your channel username)",
"4. Wait for confirmation - the connection will appear in your dashboard",
"Tip: If your channel has no public username, forward a message from it along with the code"
]
}
```
The user then does 3 things in Telegram:
1. Adds `@ZernioScheduleBot` to the channel as an administrator with the Post Messages permission, or to the group as an administrator.
2. Opens a private chat with [@ZernioScheduleBot](https://t.me/ZernioScheduleBot).
3. Sends the code with the channel username: `ZRN-ABC123 @yourchannel`. For a private channel without a username, they forward any message from the channel to the bot together with the code.
Poll `PATCH /v1/connect/telegram` with the code until `status` is `connected`:
```typescript
const { data: status } = await zernio.connect.telegram.completeTelegramConnect({
query: { code: access.code }
});
console.log(status.status);
```
```python
status = client.connect.complete_telegram_connect(code=access["code"])
print(status["status"])
```
```bash
curl -X PATCH "https://zernio.com/api/v1/connect/telegram?code=ZRN-ABC123" \
-H "Authorization: Bearer $ZERNIO_API_KEY"
```
Response (`200`) while the user has not sent the code yet:
```json
{
"status": "pending",
"expiresAt": "2027-01-01T12:15:00.000Z",
"expiresIn": 542
}
```
Response (`200`) once the bot has received it:
```json
{
"status": "connected",
"chatId": "-1001234567890",
"chatTitle": "My Channel",
"chatType": "channel",
"account": {
"_id": "66b2e19d8c3f5a7e9d0b1c2d",
"platform": "telegram",
"username": "mychannel",
"displayName": "My Channel"
}
}
```
`account._id` is the `accountId` for every call below. A code that is not used within 15 minutes returns `status: "expired"`; generate a new one.
### Direct connection
If the bot is already an administrator and you know the chat id, skip the code and [connect directly](/connect/initiate-telegram-connect) with `POST /v1/connect/telegram`:
```typescript
const { data: connected } = await zernio.connect.telegram.initiateTelegramConnect({
body: {
profileId: '66a1f0c2a4b9d3e8f1a2b3c4',
chatId: '-1001234567890'
}
});
console.log(connected.account._id);
```
```python
connected = client.connect.initiate_telegram_connect(
profile_id="66a1f0c2a4b9d3e8f1a2b3c4",
chat_id="-1001234567890"
)
print(connected["account"]["_id"])
```
```bash
curl -X POST https://zernio.com/api/v1/connect/telegram \
-H "Authorization: Bearer $ZERNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"profileId": "66a1f0c2a4b9d3e8f1a2b3c4",
"chatId": "-1001234567890"
}'
```
Response (`200`):
```json
{
"message": "Telegram channel connected successfully",
"account": {
"_id": "66b2e19d8c3f5a7e9d0b1c2d",
"platform": "telegram",
"username": "mychannel",
"displayName": "My Channel",
"isActive": true,
"chatType": "channel"
}
}
```
`chatId` is the username with an `@` prefix for a public channel (`@mychannel`) or the numeric id for a private channel or a group. To find a numeric id, forward a message from the channel to [@userinfobot](https://t.me/userinfobot), which replies with the id (private channel ids start with `-100`), or add that bot to the group for a moment and remove it once it has shown the group's negative id.
## Publish
A plain post becomes a text message. One media item becomes a photo, video or document message with `content` as its caption, and 2 to 10 items become an album with the caption on the first message. Text is parsed as HTML unless `parseMode` says otherwise.
### Text message
```typescript
const { data: published } = await zernio.posts.createPost({
body: {
content: 'Release 2.3 is out. \n\nRead the changelog .',
platforms: [
{
platform: 'telegram',
accountId: '66b2e19d8c3f5a7e9d0b1c2d',
platformSpecificData: { parseMode: 'HTML' }
}
],
publishNow: true
}
});
console.log(published.post.platforms[0].platformPostUrl);
```
```python
published = client.posts.create_post(
content='Release 2.3 is out. \n\nRead the changelog .',
platforms=[
{
"platform": "telegram",
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"platformSpecificData": {"parseMode": "HTML"}
}
],
publish_now=True
)
print(published["post"]["platforms"][0]["platformPostUrl"])
```
```bash
curl -X POST https://zernio.com/api/v1/posts \
-H "Authorization: Bearer $ZERNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "Release 2.3 is out. \n\nRead the changelog .",
"platforms": [
{
"platform": "telegram",
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"platformSpecificData": {"parseMode": "HTML"}
}
],
"publishNow": true
}'
```
Response (`201`):
```json
{
"post": {
"_id": "65f1c0a9e2b5af0012ab34cd",
"status": "published",
"platforms": [
{
"platform": "telegram",
"status": "published",
"platformPostUrl": "https://t.me/mychannel/42"
}
]
}
}
```
Every sample below changes only the `mediaItems` or the `platforms` entry of this request.
### Photo message
A single image with `content` as the caption (1,024 characters):
```json
"mediaItems": [
{ "type": "image", "url": "https://cdn.example.com/launch.jpg" }
]
```
### Video message
A single video with `content` as the caption:
```json
"mediaItems": [
{ "type": "video", "url": "https://cdn.example.com/demo.mp4" }
]
```
### Document message
Any file type, sent as a document:
```json
"mediaItems": [
{ "type": "document", "url": "https://cdn.example.com/report.pdf" }
]
```
### Album
Up to 10 items, images and videos mixed, sent as one album:
```json
"mediaItems": [
{ "type": "image", "url": "https://cdn.example.com/photo1.jpg" },
{ "type": "image", "url": "https://cdn.example.com/photo2.jpg" },
{ "type": "video", "url": "https://cdn.example.com/clip.mp4" },
{ "type": "image", "url": "https://cdn.example.com/photo3.jpg" }
]
```
### Formatting
`parseMode` selects how Telegram reads `content`. HTML is the default:
```html
bold
italic
underline
strikethrough
inline code
code block
link
```
`"Markdown"` supports `*bold*`, `_italic_`, `[link](https://example.com)` and backtick-wrapped inline code. `"MarkdownV2"` adds `__underline__`, `~strikethrough~` and `||spoiler||`, and needs a backslash before every one of these characters:
```text
_ * [ ] ( ) ~ ` > # + - = | { } . !
```
An unescaped character fails the post with "Can't parse entities".
### Silent and protected messages
`disableNotification` delivers the message without a notification sound, `protectContent` stops recipients from forwarding or saving it, and `disableWebPagePreview` suppresses the link preview:
```json
{
"platform": "telegram",
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"platformSpecificData": {
"parseMode": "HTML",
"disableWebPagePreview": true,
"disableNotification": true,
"protectContent": true
}
}
```
### Channels and groups
| Destination | Shown as the author |
|-------------|---------------------|
| Channel | The channel's name and logo |
| Group | ZernioScheduleBot |
### Edit a published message
[Edit post](/posts/edit-post) replaces the text of a published Telegram message. Messages stay editable with no time window and no limit on the number of edits, and the message id does not change. What `content` replaces depends on the original post:
| Original post | What `content` replaces | Limit |
|---------------|-------------------------|-------|
| Text only | The message text | 4,096 characters |
| Single photo, video or document | The caption | 1,024 characters |
| Album | The caption on the album (its first message) | 1,024 characters |
The media itself cannot be swapped, added or removed, and an edit with unchanged content is rejected with `400`. Check which limit applies before sending: a 2,000-character edit fits a text message and is too long for a photo caption.
```typescript
const { data: edited } = await zernio.posts.editPost({
path: { postId: published.post._id },
body: {
platform: 'telegram',
content: 'Updated. The webinar starts at 18:00 CET.'
}
});
console.log(edited.id, edited.url);
```
```python
edited = client.posts.edit_post(
post_id=published["post"]["_id"],
platform="telegram",
content="Updated. The webinar starts at 18:00 CET."
)
print(edited["id"], edited["url"])
```
```bash
curl -X POST https://zernio.com/api/v1/posts/65f1c0a9e2b5af0012ab34cd/edit \
-H "Authorization: Bearer $ZERNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"platform": "telegram",
"content": "Updated. The webinar starts at 18:00 CET."
}'
```
Response (`200`):
```json
{
"success": true,
"id": "42",
"url": "https://t.me/mychannel/42"
}
```
If the post went to several Telegram channels or groups, pass `accountId` to pick which copy to edit. Without it, the first `telegram` entry on the post is edited.
## Platform fields
All fields go in `platformSpecificData` on the Telegram entry.
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `parseMode` | `"HTML"`, `"Markdown"`, `"MarkdownV2"` | `"HTML"` | How Telegram parses `content`. See [Formatting](#formatting). |
| `disableWebPagePreview` | boolean | `false` | No link preview for URLs in the message. |
| `disableNotification` | boolean | `false` | Deliver silently: recipients get the message without a notification sound. |
| `protectContent` | boolean | `false` | Recipients cannot forward or save the message. |
## Media requirements
Files above these limits are compressed automatically: images above 10 MB and videos above 50 MB, which are Telegram's own Bot API caps. Compression has a ceiling of its own: a video over 200 MB is passed through untouched, and Telegram then refuses it.
### Images
| Property | Requirement |
|----------|-------------|
| Max per album | 10 |
| Formats | JPEG, PNG, GIF, WebP |
| Max file size | 10 MB (auto-compressed) |
| Max resolution | Width plus height must total 10,000 px or less; a larger image is downscaled |
### Videos
| Property | Requirement |
|----------|-------------|
| Max per album | 10 |
| Formats | MP4, MOV |
| Max file size | 50 MB (auto-compressed) |
| Max duration | No limit |
| Codec | H.264 recommended |
Media URLs must be direct HTTPS URLs that are publicly accessible with no redirects; Telegram rejects HTTP URLs and redirecting links with "Wrong file identifier/HTTP URL specified". Upload files through the [media endpoint](/guides/media-uploads) to get a URL that qualifies.
## Analytics
Telegram's Bot API exposes no analytics, so there are none for Telegram accounts. View counts for channel posts are visible in the Telegram app, and channels with 500 or more subscribers get Telegram's own channel statistics there.
## Inbox
Telegram supports DMs with attachments, inline and reply keyboards, and a bot command menu. There are no comments.
### Direct messages
| Feature | Supported |
|---------|-----------|
| List conversations | |
| Fetch messages | |
| Send text messages | |
| Send attachments | (images, videos, documents) |
| Edit messages | (text and inline keyboard) |
| Inline keyboards | (buttons with callback data or URLs) |
| Reply keyboards | (one-time custom keyboards) |
| Reply to a message | (`replyTo` with the message id) |
| Archive and unarchive | |
Attachments:
| Type | Max size |
|------|----------|
| Images | 10 MB |
| Videos | 50 MB |
| Documents | 50 MB |
When a user taps an inline keyboard button, the callback data arrives on the incoming message in `metadata.callbackData`. The [Messages API](/messages/list-inbox-conversations) has every endpoint.
### Bot commands
Bot commands are the entries in the "/" menu users see when they chat with the bot. Manage them with `GET`, `PUT` and `DELETE /v1/accounts/{accountId}/telegram-commands` ([Account settings](/account-settings/get-telegram-commands)): up to 100 commands, each a lowercase name of at most 32 characters (`a-z`, `0-9`, `_`, no leading slash) with a description of at most 256 characters.
### Webhooks
| Event | When it fires |
|-------|---------------|
| `message.received` | New incoming message to the bot |
| `message.sent` | Outgoing message is sent |
| `message.edited` | The user edits a sent message; also fires for `edited_channel_post` in channels where the bot is an administrator |
| `reaction.received` | A participant adds or removes an emoji reaction. The bot must be an administrator of the chat; reactions in private chats are not delivered |
Messages are stored locally as webhooks arrive; the [webhooks page](/webhooks) has the payloads. Telegram's Bot API exposes no deletion or read-receipt events for regular bot chats. Delivery and read tracking exist only in the separate Telegram Business integration, which Zernio does not use.
## What you cannot do
Telegram's Bot API does not expose:
- Polls or quizzes
- Telegram's own scheduled messages (schedule with `scheduledFor` instead)
- Replacing the media on a published post (only the text or caption can be edited)
- Managing channel administrators
- Message analytics
- Pinning messages
- Channel invite links
## Common errors
| Error | Cause | Fix |
|-------|-------|-----|
| "Bot is not a member of the channel" | `@ZernioScheduleBot` is not in the channel or group, or is not an administrator | Add the bot as an administrator and grant the Post Messages permission. |
| "Message is too long" | Text over 4,096 characters, or a caption over 1,024 | Shorten the content or split it into several messages. |
| "Wrong file identifier/HTTP URL specified" | The media URL is not reachable, uses HTTP, or redirects | Use a direct, public HTTPS URL with no redirects. |
| "Can't parse entities" | Broken HTML or Markdown, or an unescaped MarkdownV2 character | Close every tag in HTML mode; escape the reserved characters in MarkdownV2. |
| Media not displayed | An unsupported format, or a video over 200 MB, which is too large for Zernio to compress under Telegram's 50 MB cap | Check the format; compress a video over 200 MB before you send it. |
| "Access code expired" | The code was not used within 15 minutes | Generate a new code with `GET /v1/connect/telegram`. |
A `publishNow: true` post that Telegram rejects returns `207` with `post.status: "failed"` and the message in `platforms[].errorMessage`:
```json
{
"message": "Post created but publishing failed",
"error": "All platforms failed",
"post": {
"_id": "65f1c0a9e2b5af0012ab34cd",
"status": "failed",
"platforms": [
{
"platform": "telegram",
"status": "failed",
"errorMessage": "Bot is not a member of the channel"
}
]
}
}
```
`207` is a 2xx status, so `fetch(...).ok` is `true`; branch on the status code and on `post.status`. Make the bot an administrator with post permission, then retry. [Error handling](/guides/error-handling) covers the envelope.
## Related
- [Connecting accounts](/guides/connecting-accounts#telegram): the access code flow next to the OAuth platforms.
- [Create post](/posts/create-post): every field of the request.
- [Edit post](/posts/edit-post): edit the text or caption of a published message.
- [Media uploads](/guides/media-uploads): upload images and videos instead of hosting them.
- [Messages](/messages/list-inbox-conversations) and [Account settings](/account-settings/get-telegram-commands): the inbox API and bot commands.
- [Pricing](/pricing): what the inbox and outbound messages cost.
---
# Threads
Publish text, image, video and carousel posts and thread sequences to Threads with the Zernio API, with topic tags, first comments and reply management.
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
Publish text, image, video and carousel posts and thread sequences to Threads with `POST /v1/posts` and `platform: "threads"`. The same account serves analytics and reply management in the inbox.
## Quick reference
| Property | Value |
|----------|-------|
| Character limit | 500 |
| Images per post | 10 (carousel) |
| Videos per post | 1 |
| Image formats | JPEG, PNG |
| Image max size | 8 MB (auto-compressed) |
| Video format | MP4 (H.264/AAC) |
| Video max size | 1 GB |
| Video max duration | 5 minutes |
| Post types | Text, Image, Video, Carousel, Thread sequence |
| Scheduling | Yes |
| Inbox (comments) | Comment, reply, delete, hide and unhide |
| Inbox (DMs) | No |
| Analytics | Yes (views, likes, comments, shares) |
## Before you start
Threads requires an Instagram Business or Creator account with Threads enabled. The account connects through Instagram authentication (the same Facebook app), so losing Instagram access means losing Threads, and a restricted Instagram account cannot publish to Threads either. An account can publish 250 API posts per 24 hours.
Threads posts are at most 500 characters. A caption cross-posted from LinkedIn (3,000), Facebook (63,206) or Instagram (2,200) fails with "Param text must be at most 500 characters long." Send a shorter Threads version in `customContent`.
## Connect
Call `GET /v1/connect/threads` with `profileId` on [Get OAuth connect URL](/connect/get-connect-url). The user authorizes with the Instagram account that owns the Threads profile. The [connecting accounts guide](/guides/connecting-accounts) covers the OAuth flow and [scopes](/guides/connecting-accounts#scopes) in general; [Account health](/accounts/get-all-accounts-health) reports what a connected account can do with the scopes the user granted.
### OAuth scopes
| Scope | What it enables |
|-------|-----------------|
| `threads_basic` | Account identity and basic profile data |
| `threads_content_publish` | Publish threads and carousels |
| `threads_read_replies` | Read replies to your threads |
| `threads_manage_replies` | Reply to, hide and manage replies |
| `threads_manage_insights` | Post and account analytics |
| `threads_delete` | Delete threads through the API |
## Publish
A plain post becomes a text post. Add `mediaItems` for an image, a video or a carousel, and `threadItems` in `platformSpecificData` for a chain of connected posts.
### Text post
Threads accepts text-only posts of up to 500 characters, with no media required.
```typescript
import Zernio from '@zernio/node';
const zernio = new Zernio();
const { data: published } = await zernio.posts.createPost({
body: {
content: 'Hot take: the best API is the one with the best docs.',
platforms: [
{ platform: 'threads', accountId: '66b2e19d8c3f5a7e9d0b1c2d' }
],
publishNow: true
}
});
console.log(published.post.platforms[0].platformPostUrl);
```
```python
from zernio import Zernio
client = Zernio()
published = client.posts.create_post(
content="Hot take: the best API is the one with the best docs.",
platforms=[
{"platform": "threads", "accountId": "66b2e19d8c3f5a7e9d0b1c2d"}
],
publish_now=True
)
print(published["post"]["platforms"][0]["platformPostUrl"])
```
```bash
curl -X POST https://zernio.com/api/v1/posts \
-H "Authorization: Bearer $ZERNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "Hot take: the best API is the one with the best docs.",
"platforms": [
{"platform": "threads", "accountId": "66b2e19d8c3f5a7e9d0b1c2d"}
],
"publishNow": true
}'
```
Response (`201`):
```json
{
"post": {
"_id": "65f1c0a9e2b5af0012ab34cd",
"status": "published",
"platforms": [
{
"platform": "threads",
"status": "published",
"platformPostUrl": "https://www.threads.net/@yourhandle/post/..."
}
]
}
}
```
Every sample below changes only the `mediaItems` or the `platforms` entry of this request.
### Image post
A single image with an optional caption:
```json
"mediaItems": [
{ "type": "image", "url": "https://cdn.example.com/office.jpg" }
]
```
### Video post
A single video with an optional caption, at most 1 GB and 5 minutes:
```json
"mediaItems": [
{ "type": "video", "url": "https://cdn.example.com/launch.mp4" }
]
```
### Carousel
Up to 10 images in one swipeable post:
```json
"mediaItems": [
{ "type": "image", "url": "https://cdn.example.com/feature1.jpg" },
{ "type": "image", "url": "https://cdn.example.com/feature2.jpg" },
{ "type": "image", "url": "https://cdn.example.com/feature3.jpg" }
]
```
### Thread sequence
`threadItems` publishes a chain of connected posts. The first item is the root post and each later item is a reply to the one before it, in order; every item carries its own text and optional media. With `threadItems` set, the top-level `content` is stored for display and search only and is not published, so the first post must be `threadItems[0]`.
```json
{
"platform": "threads",
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"platformSpecificData": {
"threadItems": [
{
"content": "Here is a thread about API design",
"mediaItems": [{ "type": "image", "url": "https://cdn.example.com/cover.jpg" }]
},
{ "content": "1/ First, REST principles." },
{
"content": "2/ Authentication. Here is what we recommend.",
"mediaItems": [{ "type": "image", "url": "https://cdn.example.com/auth-diagram.jpg" }]
},
{ "content": "3/ Finally, always version your API. /end" }
]
}
}
```
### Topic tags
`topic_tag` sets the post's topic tag for discoverability. It is 1 to 50 characters, cannot contain periods (`.`) or ampersands (`&`), and overrides the tag Threads would extract from hashtags in the content.
```json
{
"platform": "threads",
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"platformSpecificData": { "topic_tag": "APIDesign" }
}
```
## Platform fields
All fields go in `platformSpecificData` on the Threads entry.
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `topic_tag` | string | (from hashtags) | Topic tag for categorisation and discoverability. 1 to 50 characters, no periods (`.`) or ampersands (`&`). Overrides auto-extraction from content hashtags. |
| `firstComment` | string | | Posted right after publishing as a reply to the published post (with `threadItems`, a reply to the root post). Up to 500 characters. It is itself a Threads post, so it counts toward the 250 posts per 24 hours. |
| `threadItems` | Array\<\{content, mediaItems?\}\> | | The complete sequence of posts in a thread. The first item becomes the root post and must be `threadItems[0]`. When set, the top-level `content` is for display and search only and is not published. |
## Media requirements
Images above 8 MB are compressed automatically. Original files are preserved.
### Images
| Property | Requirement |
|----------|-------------|
| Max images | 10 per post (carousel) |
| Formats | JPEG, PNG |
| Max file size | 8 MB per image (auto-compressed) |
| Recommended | 1080 x 1350 px (4:5 portrait) |
| Ratio | Dimensions | Notes |
|-------|------------|-------|
| 4:5 | 1080 x 1350 px | Portrait, recommended |
| 1:1 | 1080 x 1080 px | Square |
| 16:9 | 1080 x 608 px | Landscape |
### Videos
| Property | Requirement |
|----------|-------------|
| Max videos | 1 per post |
| Format | MP4 (H.264/AAC) |
| Max file size | 1 GB |
| Max duration | 5 minutes |
| Aspect ratio | 9:16 (vertical), 16:9 (landscape), 1:1 (square) |
| Resolution | 1080p recommended |
| Codec | H.264 |
| Frame rate | 30 fps recommended |
| Audio | AAC, 128 kbps |
### Media URLs
Threads fetches media the same way Instagram does. A media URL must be publicly accessible with no authentication, return the media bytes with the correct `Content-Type` header, and not redirect to an HTML page; Google Drive, Dropbox, OneDrive and iCloud sharing links return a webpage and fail. WebP images may fail, so use JPEG or PNG. To host files instead, use the [media endpoint](/guides/media-uploads).
## Analytics
Call `GET /v1/analytics?platform=threads` ([Analytics API](/analytics/get-analytics)).
| Metric | Available |
|--------|-----------|
| Likes | |
| Comments | |
| Shares | |
| Views | |
Threads' insights expose no impressions metric, so `views` is the reach figure to report. `shares` adds reposts and quotes together.
```typescript
const { data: analytics } = await zernio.analytics.getAnalytics({
query: { platform: 'threads', fromDate: '2026-08-01', toDate: '2026-08-31' }
});
console.log(analytics.posts);
```
```python
analytics = client.analytics.get_analytics(
platform="threads",
from_date="2026-08-01",
to_date="2026-08-31"
)
print(analytics["posts"])
```
```bash
curl "https://zernio.com/api/v1/analytics?platform=threads&fromDate=2026-08-01&toDate=2026-08-31" \
-H "Authorization: Bearer $ZERNIO_API_KEY"
```
Response (`200`), one entry per post:
```json
{
"posts": [
{
"_id": "65f1c0a9e2b5af0012ab34cd",
"platform": "threads",
"status": "published",
"publishedAt": "2026-08-14T10:00:05Z",
"platformPostUrl": "https://www.threads.net/@yourhandle/post/ABC123xyz",
"analytics": {
"likes": 164,
"comments": 19,
"shares": 12,
"views": 8210,
"engagementRate": 2.38
}
}
]
}
```
## Inbox
Threads supports comments only: you can comment, reply, delete, hide and unhide, but not like a comment. A comment on Threads is a reply to the thread, so omitting `commentId` replies to the post itself. Threads has no DMs.
| Feature | Supported |
|---------|-----------|
| List comments on posts | |
| Post a new top-level comment | (omit `commentId` and the reply lands on the post) |
| Reply to comments | |
| Delete comments | |
| Like comments | |
| Hide and unhide comments | |
Hide a reply with `POST /v1/inbox/comments/{postId}/{commentId}/hide` and unhide it with `DELETE` on the same path; the [Comments API](/comments/list-inbox-comments) lists every endpoint.
## What you cannot do
Threads' API does not expose:
- Polls
- GIF search
- Editing a post after publishing
- Who liked or reposted a post
- Quote posts
- Liking or unliking comments
- DMs
## Common errors
| Error | Cause | Fix |
|-------|-------|-----|
| "Param text must be at most 500 characters long." | The post exceeds 500 characters | Shorten to 500 characters. Use `customContent` on cross-platform posts so each platform gets its own version. |
| "Media download has failed. The media URI doesn't meet our requirements." (2207052) | Threads cannot fetch media from the URL | The URL must return media bytes, not an HTML page. WebP may fail; use JPEG or PNG. |
| "Instagram account is restricted." (2207050) | The linked Instagram account is restricted | Check the account status on Instagram and resolve any policy violation before retrying. |
| "Publishing failed due to max retries reached" | Every publishing retry was exhausted | Usually temporary. Wait a few minutes and retry. |
A `publishNow: true` post that Threads rejects returns `207` with `post.status: "failed"` and the message in `platforms[].errorMessage`:
```json
{
"message": "Post created but publishing failed",
"error": "All platforms failed",
"post": {
"_id": "65f1c0a9e2b5af0012ab34cd",
"status": "failed",
"platforms": [
{
"platform": "threads",
"status": "failed",
"errorMessage": "Param text must be at most 500 characters long."
}
]
}
}
```
`207` is a 2xx status, so `fetch(...).ok` is `true`; branch on the status code and on `post.status`. [Error handling](/guides/error-handling) covers the envelope.
## Related
- [Connecting accounts](/guides/connecting-accounts): the OAuth flow through Instagram.
- [Create post](/posts/create-post): every field of the request.
- [Media uploads](/guides/media-uploads): upload images and videos instead of hosting them.
- [Analytics](/analytics/get-analytics): post performance metrics.
- [Comments](/comments/list-inbox-comments): the inbox API for replies.
- [Pricing](/pricing): what analytics, the inbox and outbound messages cost, and which replies count.
---
# TikTok Ads
Run Spark Ads, standalone video campaigns and website conversion campaigns on a tiktokads account, with Custom Audiences and Insights.
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
Boost a TikTok video as a Spark Ad with `POST /v1/ads/boost`, or create a campaign, ad group and video ad in one `POST /v1/ads/create` call, on a `tiktokads` account. No TikTok Business Center developer onboarding is needed: connect with OAuth and call the ads endpoints. Zernio's `adSetId` is the TikTok ad group id (on Meta the same field is an ad set, on LinkedIn a campaign).
## Quick reference
| Feature | Status |
|---------|--------|
| Standalone campaigns (Campaign > Ad Group > Ad) | Yes |
| Website conversion ads (TikTok Pixel optimization) | Yes |
| Spark Ads (boost organic videos) | Yes |
| Spark Ad custom destination URL and CTA | Yes |
| Spark Code (cross-creator boosts via `auth_code`) | Yes |
| Bid strategy (Cost Cap, ROAS floor) | Yes |
| Campaign duplication (manual graph copy) | Yes |
| Attach to an existing ad group on `/v1/ads/create` | Yes |
| Creative swap on `PUT /v1/ads/{adId}` | Yes |
| Targeting updates after creation | Yes |
| Agency Business Centers (multi-advertiser plus a BC list endpoint) | Yes |
| Custom Audiences (customer list) | Yes |
| Age, gender, location and interest targeting | Yes |
| Video creative from URL | Yes |
| Insights (spend, views, CTR, CPM) | Yes |
| Catalog and TikTok Shop ads | Roadmap |
| Chunked video upload and async transcode | Roadmap |
## Before you start
TikTok Ads requires a `tiktokads` account with its own token: the TikTok Business API is a separate OAuth from TikTok posting. It does not use per-request OAuth scopes; when the user authorizes the connection, permissions are granted at the app level (ad account management, reporting, creative management) rather than as individual scopes in the consent URL. You also need the advertiser id, from [List ad accounts](/ad-accounts/list-ad-accounts).
## Connect
Call `GET /v1/connect/tiktok/ads` with `profileId` and, optionally, the `accountId` of a TikTok posting account ([Connect ads](/connect/connect-ads)). With `accountId`, the new `tiktokads` account links to that posting account, so Spark Ads and standalone ads with the real `@username` identity become available. Without it, ads-only mode applies: standalone ads use a synthetic Brand Identity (`CUSTOMIZED_USER`, configured with `PATCH /v1/connect/tiktok-ads` or inline through `brandIdentity` on create) and Spark Ads are unavailable, because TikTok requires a posting account for them.
```bash
curl "https://zernio.com/api/v1/connect/tiktok/ads?profileId=66a1f0c2a4b9d3e8f1a2b3c4&accountId=66b2e19d8c3f5a7e9d0b1c2d" \
-H "Authorization: Bearer $ZERNIO_API_KEY"
```
Response (`200`):
```json
{
"authUrl": "https://business-api.tiktok.com/portal/auth?app_id=...",
"state": "..."
}
```
Send the user to `authUrl`, as in the [connecting accounts guide](/guides/connecting-accounts). Once the flow completes, the same call returns `alreadyConnected: true` with the `tiktokads` `accountId`, which every sample below uses.
## Create ads
### Spark Ads (boost)
Call `POST /v1/ads/boost` with `postId`, `accountId`, `adAccountId`, `goal` and `budget`. Spark Ads keep the creator's identity, organic engagement signals and follower handle.
```typescript
import Zernio from '@zernio/node';
const zernio = new Zernio();
const { data: boosted } = await zernio.adcampaigns.boostPost({
body: {
postId: '65f1c0a9e2b5af0012ab34cd',
accountId: '66b2e19d8c3f5a7e9d0b1c2d',
adAccountId: '7123456789012345678',
name: 'Boost viral video',
goal: 'traffic',
budget: { amount: 50, type: 'daily' },
schedule: { startDate: '2027-01-04T09:00:00Z', endDate: '2027-01-11T23:59:00Z' },
targeting: { countries: ['US'] },
linkUrl: 'https://example.com/landing',
callToAction: 'SHOP_NOW',
bidStrategy: 'COST_CAP',
bidAmount: 0.5
}
});
console.log(boosted.ad._id);
```
```python
from zernio import Zernio
client = Zernio()
boosted = client.ad_campaigns.boost_post(
post_id="65f1c0a9e2b5af0012ab34cd",
account_id="66b2e19d8c3f5a7e9d0b1c2d",
ad_account_id="7123456789012345678",
name="Boost viral video",
goal="traffic",
budget={"amount": 50, "type": "daily"},
schedule={"startDate": "2027-01-04T09:00:00Z", "endDate": "2027-01-11T23:59:00Z"},
targeting={"countries": ["US"]},
link_url="https://example.com/landing",
call_to_action="SHOP_NOW",
bid_strategy="COST_CAP",
bid_amount=0.5,
)
print(boosted["ad"]["_id"])
```
```bash
curl -X POST "https://zernio.com/api/v1/ads/boost" \
-H "Authorization: Bearer $ZERNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"postId": "65f1c0a9e2b5af0012ab34cd",
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"adAccountId": "7123456789012345678",
"name": "Boost viral video",
"goal": "traffic",
"budget": { "amount": 50, "type": "daily" },
"schedule": { "startDate": "2027-01-04T09:00:00Z", "endDate": "2027-01-11T23:59:00Z" },
"targeting": { "countries": ["US"] },
"linkUrl": "https://example.com/landing",
"callToAction": "SHOP_NOW",
"bidStrategy": "COST_CAP",
"bidAmount": 0.5
}'
```
Response (`201`):
```json
{
"ad": {
"_id": "66f0a1b2c3d4e5f6a7b8c9d0",
"name": "Boost viral video",
"platform": "tiktok",
"status": "pending_review",
"adType": "boost",
"goal": "traffic",
"budget": { "amount": 50, "type": "daily" },
"bidStrategy": "COST_CAP",
"bidAmount": 0.5,
"platformAdId": "1802345678901234567",
"platformCampaignId": "1802345678901234001",
"platformAdSetId": "1802345678901234002"
},
"message": "Ad created"
}
```
`linkUrl` and `callToAction` are the Spark Ad creative overrides, mapped to `landing_page_url` and `call_to_action` on TikTok's `/v2/ad/create/`. Pass them for traffic and conversion goals: without `linkUrl` the Spark Ad has no clickable destination distinct from the organic video. `targeting.countries` is required on TikTok boosts (the ad group needs `location_ids`). `bidStrategy` and `bidAmount` are optional Cost Cap bidding ([Bid strategy](#bid-strategy)). Every sample on this page reuses the `zernio` and `client` constructors from this one.
### Standalone campaign
Call `POST /v1/ads/create`. TikTok's ads endpoint is video-only, so the video URL goes in `imageUrl` (the field keeps its name for cross-platform consistency), `body` is the video caption, and `headline` is ignored because TikTok creatives have no headline slot. Valid `goal` values are `engagement`, `traffic`, `awareness`, `video_views`, `lead_generation`, `conversions` and `app_promotion`.
```typescript
const { data: created } = await zernio.adcampaigns.createStandaloneAd({
body: {
accountId: '66b2e19d8c3f5a7e9d0b1c2d',
adAccountId: '7123456789012345678',
name: 'Spring launch',
goal: 'traffic',
budgetAmount: 100,
budgetType: 'daily',
body: 'Spring drop is live',
linkUrl: 'https://example.com/spring',
imageUrl: 'https://cdn.example.com/launch.mp4',
callToAction: 'SHOP_NOW',
countries: ['US'],
ageMin: 18,
ageMax: 34
}
});
console.log(created.ad.platformAdSetId);
```
```python
created = client.ad_campaigns.create_standalone_ad(
account_id="66b2e19d8c3f5a7e9d0b1c2d",
ad_account_id="7123456789012345678",
name="Spring launch",
goal="traffic",
budget_amount=100,
budget_type="daily",
body="Spring drop is live",
link_url="https://example.com/spring",
image_url="https://cdn.example.com/launch.mp4",
call_to_action="SHOP_NOW",
countries=["US"],
age_min=18,
age_max=34,
)
print(created["ad"]["platformAdSetId"])
```
```bash
curl -X POST "https://zernio.com/api/v1/ads/create" \
-H "Authorization: Bearer $ZERNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"adAccountId": "7123456789012345678",
"name": "Spring launch",
"goal": "traffic",
"budgetAmount": 100,
"budgetType": "daily",
"body": "Spring drop is live",
"linkUrl": "https://example.com/spring",
"imageUrl": "https://cdn.example.com/launch.mp4",
"callToAction": "SHOP_NOW",
"countries": ["US"],
"ageMin": 18,
"ageMax": 34
}'
```
Response (`201`), same shape as the boost with `"adType": "standalone"`:
```json
{
"ad": {
"_id": "66f0a1b2c3d4e5f6a7b8c9d1",
"name": "Spring launch",
"platform": "tiktok",
"status": "pending_review",
"adType": "standalone",
"goal": "traffic",
"platformAdId": "1802345678901234568",
"platformCampaignId": "1802345678901234003",
"platformAdSetId": "1802345678901234004"
},
"message": "Ad created"
}
```
`platformAdSetId` is the new TikTok ad group.
### Conversion campaigns
A website-conversion campaign (`goal: "conversions"`, which Zernio maps to a `CONVERSIONS` objective with a `WEBSITE` / `CONVERT` ad group) needs a TikTok Pixel. Without one, TikTok rejects the ad group with `40002: Please select a pixel`. Pass the pixel through `promotedObject`, the same cross-platform field Meta uses:
| `promotedObject` field | Maps to TikTok | Required |
|---|---|---|
| `pixelId` | ad group `pixel_id` (numeric) | Yes |
| `customEventType` | ad group `optimization_event` (the pixel event to optimise for) | No (auto-bid CONVERT works without it) |
`customEventType` takes a TikTok `optimization_event` code (TikTok's own UPPER_SNAKE codes, not Meta's `PURCHASE` or `LEAD` vocabulary and not PascalCase), or the exact event name shown in TikTok Events Manager. The value must be one of the events your pixel is configured to track. Common website codes:
| Code | Pixel event |
|---|---|
| `ON_WEB_ORDER` | Complete Payment |
| `INITIATE_ORDER` | Place an Order |
| `ON_WEB_CART` | Add to Cart |
| `ON_WEB_REGISTER` | Complete Registration |
| `ON_WEB_DETAIL` | View Content |
| `FORM` | Submit Form |
| `ON_WEB_SEARCH` | Search |
| `ON_WEB_ADD_TO_WISHLIST` | Add to Wishlist |
| `LANDING_PAGE_VIEW` | Landing Page View |
`pixelId` must be the numeric TikTok Pixel id, not the alphanumeric Pixel Code shown in Events Manager (for example `D00IKHRC77UE0J0RTNHG`). TikTok's ad-group API rejects the code with `40002: pixel_id ... is not a valid integer string`. The numeric id is the `pixel_id` that TikTok's `GET /pixel/list/` returns; if Ads Manager only shows the alphanumeric code for your pixel, retrieve the numeric id through the TikTok Marketing API (`GET /pixel/list/`, filter by `code`).
Find the pixel and the events it tracks in TikTok Ads Manager under Assets, then Events. The request is the standalone create plus `promotedObject`:
```json
{
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"adAccountId": "7123456789012345678",
"name": "Spring launch, purchases",
"goal": "conversions",
"budgetAmount": 100,
"budgetType": "daily",
"body": "Spring drop is live",
"linkUrl": "https://example.com/spring",
"imageUrl": "https://cdn.example.com/launch.mp4",
"callToAction": "SHOP_NOW",
"countries": ["US"],
"ageMin": 18,
"ageMax": 34,
"promotedObject": { "pixelId": "7987654321098765432", "customEventType": "ON_WEB_ORDER" }
}
```
If a conversion ad group already exists in TikTok Ads Manager with the pixel and event configured, pass its id as `adSetId` on `POST /v1/ads/create` instead. The new creative inherits the pixel and `optimization_event` from the parent ad group, so `promotedObject` is not needed ([Attach to an existing ad group](#attach-to-an-existing-ad-group)).
### Spark Code for cross-creator boosts
To boost a creator's organic video from a different TikTok account than the one running the ads, the creator generates a Spark Code in their TikTok app's Promote settings and shares it with the advertiser. Pass it as `sparkAuthCode` on `POST /v1/ads/boost`, with the creator's video as `platformPostId`:
```json
{
"platformPostId": "7310234567890123456",
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"adAccountId": "7123456789012345678",
"name": "Cross-creator boost",
"goal": "traffic",
"budget": { "amount": 50, "type": "daily" },
"targeting": { "countries": ["US"] },
"linkUrl": "https://example.com",
"callToAction": "SHOP_NOW",
"sparkAuthCode": "BCAQAAAA..."
}
```
Without `sparkAuthCode`, boosts are limited to videos owned by the same TikTok account that runs the ads. The value maps to `auth_code` on TikTok's `AdcreateCreatives`.
### Bid strategy
Pass `bidStrategy` (the cross-platform Meta enum) on `POST /v1/ads/boost`, `POST /v1/ads/create` or `PUT /v1/ads/ad-sets/{adSetId}`. Zernio maps it to TikTok's `bid_type`, `bid_price` and `deep_bid_type`:
| `bidStrategy` | Maps to TikTok | Required field |
|---|---|---|
| `LOWEST_COST_WITHOUT_CAP` (default) | `bid_type: BID_TYPE_NO_BID` | none |
| `LOWEST_COST_WITH_BID_CAP` | `bid_type: BID_TYPE_CUSTOM` plus `bid_price` | `bidAmount` |
| `COST_CAP` | `bid_type: BID_TYPE_CUSTOM` plus `bid_price` | `bidAmount` |
| `LOWEST_COST_WITH_MIN_ROAS` | `bid_type: BID_TYPE_NO_BID` plus `deep_bid_type: MIN_ROAS` | `roasAverageFloor` (the account must be value-optimization-enabled) |
`bidAmount` is in whole currency units of the ad account (USD: `5` is $5.00). On reads (`GET /v1/ads/tree`, `GET /v1/ads/campaigns`), TikTok's native `bid_type` is normalized back to the same enum, so cross-platform consumers see one shape regardless of source platform.
### Media requirements
| Type | Format | Max size | Notes |
|------|--------|----------|-------|
| Video | MP4, MOV, MPEG | 500 MB | 9:16 vertical, 720p or better, 5 to 60 seconds |
| Image | JPEG, PNG | 30 MB | 1080 x 1920 for full-screen |
Videos are fetched from the URL you pass; the [media uploads guide](/guides/media-uploads) covers hosting them on Zernio.
## Target
### Custom Audiences
Create a customer-file Custom Audience with `POST /v1/ads/audiences` (`type: "customer_list"`), then upload members with `POST /v1/ads/audiences/{audienceId}/users`. `adAccountId` is the TikTok advertiser id. TikTok matches on email only (any `phone` is ignored); values are SHA-256 hashed server-side.
TikTok needs the member file at creation time, so the audience is created lazily on the first member upload: the create records it with status `pending` (no platform id yet), and the first `users` call provisions it on TikTok. A new audience takes up to 48 hours to finish processing before it is targetable.
```typescript
const { data: audience } = await zernio.adaudiences.createAdAudience({
body: {
accountId: '66b2e19d8c3f5a7e9d0b1c2d',
adAccountId: '7123456789012345678',
type: 'customer_list',
name: 'Cart abandoners'
}
});
const { data: upload } = await zernio.adaudiences.addUsersToAdAudience({
path: { audienceId: audience.audience.id },
body: { users: [{ email: 'jane@example.com' }, { email: 'sam@example.com' }] }
});
console.log(upload.numReceived);
```
```python
audience = client.ad_audiences.create_ad_audience(
account_id="66b2e19d8c3f5a7e9d0b1c2d",
ad_account_id="7123456789012345678",
type="customer_list",
name="Cart abandoners",
)
upload = client.ad_audiences.add_users_to_ad_audience(
audience_id=audience["audience"]["id"],
users=[{"email": "jane@example.com"}, {"email": "sam@example.com"}],
)
print(upload["numReceived"])
```
```bash
curl -X POST "https://zernio.com/api/v1/ads/audiences" \
-H "Authorization: Bearer $ZERNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "accountId": "66b2e19d8c3f5a7e9d0b1c2d", "adAccountId": "7123456789012345678", "type": "customer_list", "name": "Cart abandoners" }'
curl -X POST "https://zernio.com/api/v1/ads/audiences/66e5b2c3d4f5a6b7c8d9e0f1/users" \
-H "Authorization: Bearer $ZERNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "users": [{ "email": "jane@example.com" }, { "email": "sam@example.com" }] }'
```
Response (`201`):
```json
{
"audience": { "id": "66e5b2c3d4f5a6b7c8d9e0f1", "name": "Cart abandoners", "type": "customer_list", "platform": "tiktok", "status": "pending" },
"message": "Audience created"
}
```
Response (`200`):
```json
{ "message": "Users added", "numReceived": 2, "numInvalid": 0 }
```
### Targeting fields
`countries`, `ageMin`, `ageMax`, `gender`, `interests` (from `GET /v1/ads/interests`), `behaviors`, `regions`, `cities`, `zips`, `metros` and `incomeTier` apply on create; `audienceInclude` and `audienceExclude` take the `platformAudienceId` of a processed Custom Audience. To change targeting after creation, see [Targeting update after creation](#targeting-update-after-creation).
## Measure
Call `GET /v1/ads/{adId}/analytics` with the ad's `_id` for spend, impressions, clicks, CTR, CPM and video metrics, and `breakdowns` for a split by `gender`, `age`, `country_code`, `platform`, `ac` or `language`.
```bash
curl "https://zernio.com/api/v1/ads/66f0a1b2c3d4e5f6a7b8c9d0/analytics?fromDate=2027-01-04&toDate=2027-01-11&breakdowns=age" \
-H "Authorization: Bearer $ZERNIO_API_KEY"
```
Response (`200`), trimmed:
```json
{
"ad": { "id": "66f0a1b2c3d4e5f6a7b8c9d0", "name": "Boost viral video", "platform": "tiktok", "status": "active", "currency": "USD" },
"analytics": {
"summary": { "spend": 312.4, "impressions": 128400, "clicks": 2110, "ctr": 1.64, "cpc": 0.15, "cpm": 2.43, "videoPlayActions": 96200, "videoP100WatchedActions": 18400 },
"daily": [ { "date": "2027-01-04", "spend": 44.1, "impressions": 18200, "clicks": 301 } ],
"breakdowns": { "age": [ { "value": "AGE_18_24", "spend": 140.2, "impressions": 61000, "clicks": 1080 } ] }
}
}
```
`GET /v1/ads/tree` rolls the same metrics up per campaign and ad group ([Get campaign tree](/ad-campaigns/get-ad-tree)). For offline conversions (in-store, CRM, call-center), `POST /v1/ads/conversions` on a `tiktokads` account sends them to TikTok's Offline Events API: `destinationId` is the Offline Event Set id, each event must carry an email or phone, and the connection must have granted the Offline Events permission (older grants must reconnect). Web-pixel events are not sent this way ([Send conversions](/conversions/send-conversions)).
## Operate
### Attach to an existing ad group
`POST /v1/ads/create` with `adSetId` creates a new ad inside an existing TikTok ad group, skipping the campaign and ad-group create. Bid strategy, budget, targeting and goal are inherited from the ad group. Use it when one ad group was configured by hand in TikTok Ads Manager and you want to add Zernio-managed creatives to it.
### Ad comments
`GET /v1/ads/{adId}/comments` returns the comments on a TikTok ad in the same normalized shape it returns for Meta, and TikTok also supports moderation: reply, hide or restore, and delete. Comments on organic posts are read and moderated through the [inbox](/platforms/tiktok#comments) instead; this endpoint is the one that reaches comments on ad creatives, dark posts included.
TikTok reads comments over a date window rather than all of history, so the window is at most 30 days and defaults to the last 30 days. See [Ad comments](/platforms/meta-ads/ad-comments#tiktok-ads) for the endpoints, the window rules and the `canReply` / `canDelete` / `canHide` flags.
Listing uses the stored ad group without resolving identity or looking up the video item ID. `meta.tiktokItemId: null` does not block it, and there is no ad-level identity or item-ID check to perform first. `canReply` and `canDelete` are per comment and false when the identity or item needed to moderate is unknown. Hiding does not require an identity. A direct reply or delete can resolve missing fields lazily; use the returned capabilities to guide the UI without blocking comment reads.
### Creative swap
`PUT /v1/ads/{adId}` accepts a `creative` object that replaces the live creative on an existing TikTok ad, patch-style: only the fields you supply are touched. `headline` is ignored (no slot on TikTok), `body` becomes `ad_text`, `linkUrl` becomes `landing_page_url`, and `videoUrl` triggers a fresh upload.
```bash
curl -X PUT "https://zernio.com/api/v1/ads/66f0a1b2c3d4e5f6a7b8c9d0" \
-H "Authorization: Bearer $ZERNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "creative": { "body": "Spring drop, last week", "videoUrl": "https://cdn.example.com/launch-v2.mp4" } }'
```
Response (`200`):
```json
{
"ad": { "_id": "66f0a1b2c3d4e5f6a7b8c9d0", "platform": "tiktok", "status": "pending_review", "creative": { "body": "Spring drop, last week" } },
"message": "Ad updated"
}
```
### Targeting update after creation
`PUT /v1/ads/{adId}` accepts `targeting` for TikTok ads with the same field set as create; Zernio forwards it to `/v2/adgroup/update/`. The other networks are narrower: Pinterest and X return `501`, Google accepts keyword and device bid adjustments only, and LinkedIn accepts `countries` only.
### Campaign duplication
`POST /v1/ads/campaigns/{campaignId}/duplicate` with `platform: "tiktok"` works through a manual graph walk: Zernio reads the source campaign, ad groups and ads, then recreates each entity with bid configuration, targeting, schedule and creative fields preserved. Spark Ad linkage (`tiktok_item_id`) carries over. Everything is created paused so you can review before launching; the response carries `copiedCampaignId`.
### Agency Business Centers
Connecting a TikTok account that owns one or more Business Centers enumerates every advertiser under those BCs. There is no per-call cap: `GET /v1/ads/accounts` walks `/v2/bc/asset/get/` (paginated server-side), chunks the `/v2/advertiser/info/` lookup underneath, and returns the full roster. Solo advertisers without a BC fall back to the OAuth-time advertiser list (a single token can typically reach a handful of advertisers without a BC). The advertiser list is cached on the connection for 1 hour and refreshed on the next call after expiry. `GET /v1/ads/business-centers?accountId=` lists the BCs themselves with an `advertiserCount` each, for agency-style pickers.
## What you cannot do
TikTok Ads through Zernio does not support:
- Catalog and TikTok Shop ads. `goal` has no catalog option on TikTok, and a catalog campaign built in Ads Manager syncs back as `conversions`.
- TikTok Instant Forms. `goal: "lead_generation"` builds a Smart+ lead campaign that sends the click to your own form on `linkUrl`.
- Spark Ads on an ads-only connection, because TikTok requires a posting account for them. Connect the TikTok account itself, not the ad account alone.
- Chunked video upload and async transcode. A video creative transfers in one request, under the 500 MB ceiling in [media requirements](#media-requirements).
- Creating an ad group on its own. `POST /v1/ads/ad-sets` is Google Ads only and answers `501` everywhere else.
- The Meta-only reads and tools. Ad previews, the creative library, reach and frequency, the change log, ad labels, A/B studies, account finance, the image and video libraries and asynchronous insights reports all answer `501` on TikTok.
## Common errors
| Error | Cause | Fix |
|---|---|---|
| `422` with code `ads_connection_required` | No `tiktokads` account for the profile, or the TikTok user is not authorized as an Identity on the advertiser | [Connect](#connect) TikTok Ads, or authorize the identity in TikTok Ads Manager. |
| `40002: Please select a pixel` | `goal: "conversions"` without `promotedObject.pixelId` | Pass the numeric pixel id, or attach to an existing conversion ad group with `adSetId`. |
| `40002: pixel_id ... is not a valid integer string` | The alphanumeric Pixel Code was sent instead of the numeric id | Use the `pixel_id` from TikTok's `GET /pixel/list/`. |
| `400` on a boost | `countries` missing, or `callToAction` sent without `linkUrl` | Send `targeting.countries`, and always pair the CTA with a destination. |
| Budget rejected | Below TikTok's minimum | TikTok's minimum is $20 per ad set. |
A `422` on any ads call means the profile has no TikTok Ads connection:
```json
{
"error": "TikTok Ads is not connected for this profile",
"type": "invalid_request_error",
"code": "ads_connection_required",
"param": "accountId"
}
```
[Error handling](/guides/error-handling) covers the envelope.
## Related
- [Connecting accounts](/guides/connecting-accounts): the OAuth flow behind `authUrl`.
- [TikTok](/platforms/tiktok): the posting account Spark Ads boost from.
- [Boost post](/ad-campaigns/boost-post) and [Create standalone ad](/ad-campaigns/create-standalone-ad): every field.
- [Meta Ads](/platforms/meta-ads): the same endpoints on Meta, where `adSetId` is an ad set.
---
# TikTok
Publish videos and photo carousels to TikTok with the Zernio API, with creator privacy levels, duet and stitch controls, custom covers, AI disclosure and Creator Inbox drafts.
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
Publish videos and photo carousels to TikTok with `POST /v1/posts` and `platform: "tiktok"`. Settings go in a top-level `tiktokSettings` object. The same account also serves post and account analytics, DMs and comments.
## Quick reference
| Property | Value |
|----------|-------|
| Character limit | 2,200 (video caption), 4,000 (photo `description`) |
| Photo title | 90 characters (auto-truncated, hashtags stripped) |
| Photos per post | 35 (carousel) |
| Videos per post | 1 |
| Photo formats | JPEG, PNG, WebP |
| Photo max size | 20 MB |
| Video formats | MP4, MOV, WebM |
| Video max size | 4 GB |
| Video duration | 3 seconds to 10 minutes |
| Post types | Video, Photo carousel |
| Scheduling | Yes |
| Video privacy | Public only on TikTok Business app connections; the creator's allowed levels on developer app connections |
| Inbox (comments) | Yes (accounts connected through TikTok's Business app) |
| Inbox (DMs) | Yes (TikTok Business Accounts, replies only) |
| Analytics | Limited |
## Before you start
TikTok requires media on every post; there are no text-only posts. Every post also needs `content_preview_confirmed: true` and `express_consent_given: true` (a legal requirement from TikTok), and `privacy_level` must be one of the values TikTok reports for that creator, so read [creator info](#creator-info) before you build the request; on an account connected through TikTok's Business app, a video direct post is public and any other level is rejected ([connection lanes](#connection-lanes)). Both Zernio and TikTok cap how many posts an account can create through the API per rolling 24 hours ([daily posting caps](#daily-posting-caps)). Content moderation is stricter through the API than in the app.
A post from an account connected through TikTok's developer app can fail with "TikTok direct posting is at capacity right now" although nothing is wrong with the account or the media. TikTok caps how many distinct developer app accounts can direct-post through the Zernio app per rolling 24 hours, shared by every Zernio customer on that lane. Accounts connected through TikTok's Business app, which is every new connection, never hit it. See [Direct posting at capacity](#direct-posting-at-capacity).
## Connect
Call `GET /v1/connect/tiktok` with `profileId` on [Get OAuth connect URL](/connect/get-connect-url). The [connecting accounts guide](/guides/connecting-accounts) covers the OAuth flow and [scopes](/guides/connecting-accounts#scopes) in general; [Account health](/accounts/get-all-accounts-health) reports what a connected account can do with the scopes the user granted.
### Connection lanes
TikTok connections now go through the TikTok for Business app. An account connected earlier stays on TikTok's developer app until its owner reconnects it, and the lane is decided when the account is connected. Which lane an account is on changes three things:
- **Capacity**: developer app accounts share the app-wide [direct posting cap](#direct-posting-at-capacity), Business app accounts are exempt from it.
- **Video privacy**: Business app accounts publish videos as public only, and [Creator Inbox drafts](#draft-delivery) are the only route to a non-public video. Photo posts keep every level the creator allows on both lanes.
- **Inbox**: DMs and comment management need a Business app connection ([Inbox](#inbox)).
Reconnecting an account is what moves it to the Business app:
- One TikTok account per profile: connecting on a profile that already has a TikTok account replaces it.
- Reconnecting the same account keeps it and its history.
- Authorizing a different account takes the slot and permanently deletes the previous account's analytics, inbox and DM history.
- Zernio tells “same vs different” by the last connected `@handle`; if the handle was renamed on TikTok since the last connect, it may be treated as a different account.
> **Note:** If the authorization leaves out a permission the already-connected account needs, nothing changes and the flow returns `missing_tiktok_permissions`. Connect again and accept every permission on TikTok’s consent screen.
### OAuth scopes
| Scope | What it enables |
|-------|-----------------|
| `user.info.basic` | Account identity (username, avatar) for the connected account |
| `user.info.profile` | Extended profile info (bio, verified status, profile link) |
| `user.info.stats` | Follower, likes and video counts for account analytics |
| `video.publish` | Direct-post publishing of videos and photo posts |
| `video.upload` | Upload to the creator's TikTok inbox as a draft |
| `video.list` | Read the account's videos for post analytics |
## Publish
A plain post becomes a video post when the media is one video and a photo carousel when it is images. Send `tiktokSettings` at the top level of the request body, where it applies to every TikTok entry in `platforms`; TikTok requires it on every post. Both `snake_case` and `camelCase` field names are accepted.
### Video post
A single video between 3 seconds and 10 minutes. Vertical 9:16 is the only aspect ratio that works well on TikTok.
```typescript
import Zernio from '@zernio/node';
const zernio = new Zernio();
const { data: published } = await zernio.posts.createPost({
body: {
content: 'New cooking tutorial #recipe #foodtok',
mediaItems: [
{ type: 'video', url: 'https://cdn.example.com/cooking-tutorial.mp4' }
],
platforms: [
{ platform: 'tiktok', accountId: '66b2e19d8c3f5a7e9d0b1c2d' }
],
tiktokSettings: {
privacy_level: 'PUBLIC_TO_EVERYONE',
allow_comment: true,
allow_duet: true,
allow_stitch: true,
content_preview_confirmed: true,
express_consent_given: true
},
publishNow: true
}
});
console.log(published.post.platforms[0].status);
```
```python
from zernio import Zernio
client = Zernio()
published = client.posts.create_post(
content="New cooking tutorial #recipe #foodtok",
media_items=[
{"type": "video", "url": "https://cdn.example.com/cooking-tutorial.mp4"}
],
platforms=[
{"platform": "tiktok", "accountId": "66b2e19d8c3f5a7e9d0b1c2d"}
],
tiktok_settings={
"privacy_level": "PUBLIC_TO_EVERYONE",
"allow_comment": True,
"allow_duet": True,
"allow_stitch": True,
"content_preview_confirmed": True,
"express_consent_given": True
},
publish_now=True
)
print(published["post"]["platforms"][0]["status"])
```
```bash
curl -X POST https://zernio.com/api/v1/posts \
-H "Authorization: Bearer $ZERNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "New cooking tutorial #recipe #foodtok",
"mediaItems": [
{"type": "video", "url": "https://cdn.example.com/cooking-tutorial.mp4"}
],
"platforms": [
{"platform": "tiktok", "accountId": "66b2e19d8c3f5a7e9d0b1c2d"}
],
"tiktokSettings": {
"privacy_level": "PUBLIC_TO_EVERYONE",
"allow_comment": true,
"allow_duet": true,
"allow_stitch": true,
"content_preview_confirmed": true,
"express_consent_given": true
},
"publishNow": true
}'
```
Response (`201`):
```json
{
"post": {
"_id": "65f1c0a9e2b5af0012ab34cd",
"status": "published",
"platforms": [
{
"platform": "tiktok",
"status": "published",
"platformPostUrl": null
}
]
}
}
```
TikTok exposes the video id some minutes after publishing, so `platformPostUrl` can be empty at first; the [`post.tiktok.url_resolved`](/webhooks/posts#posttiktokurl_resolved) webhook delivers it once it exists. Every sample below changes only `mediaItems` or `tiktokSettings` of this request.
### Video cover
`video_cover_timestamp_ms` picks the frame used as the cover, and `video_cover_image_url` replaces it with a custom image (JPG, PNG or WebP, at most 20 MB). Any image URL Zernio can download works.
- For most accounts, Zernio downloads the image, rehosts it, stitches it in as a single frame at the start of the video, and TikTok uses that as the cover.
- Accounts connected through the TikTok for Business app skip the stitching: Zernio rehosts the image and hands TikTok that URL as the cover. If `video_cover_image_url` is omitted, those accounts fall back to `video_cover_timestamp_ms`.
When both are set, the image wins:
```json
"tiktokSettings": {
"privacy_level": "PUBLIC_TO_EVERYONE",
"allow_comment": true,
"allow_duet": true,
"allow_stitch": true,
"video_cover_image_url": "https://cdn.example.com/teaser-cover.jpg",
"content_preview_confirmed": true,
"express_consent_given": true
}
```
### Photo carousel
Up to 35 images. `content` becomes the photo title (90 characters, hashtags and URLs stripped), so put the full caption in `description`, which takes up to 4,000 characters. Photos and videos cannot be mixed in one post, and `allow_duet` and `allow_stitch` do not apply:
```json
"mediaItems": [
{ "type": "image", "url": "https://cdn.example.com/photo1.jpg" },
{ "type": "image", "url": "https://cdn.example.com/photo2.jpg" },
{ "type": "image", "url": "https://cdn.example.com/photo3.jpg" }
],
"tiktokSettings": {
"privacy_level": "PUBLIC_TO_EVERYONE",
"allow_comment": true,
"media_type": "photo",
"photo_cover_index": 0,
"description": "Full trip recap from our weekend across the coast. #travel #roadtrip #adventure",
"auto_add_music": true,
"content_preview_confirmed": true,
"express_consent_given": true
}
```
### Creator info
Call `GET /v1/accounts/{accountId}/tiktok/creator-info` ([Get TikTok creator info](/accounts/get-tiktok-creator-info)) before you build a post. It returns the creator's allowed `privacyLevels`, `postingLimits` with the interaction toggles, and the `commercialContentTypes` the account can disclose. A non-TikTok `accountId` returns `400`.
```typescript
const { data: info } = await zernio.accounts.getTikTokCreatorInfo({
path: { accountId: '66b2e19d8c3f5a7e9d0b1c2d' },
query: { mediaType: 'video' }
});
console.log(info.privacyLevels);
```
```python
info = client.accounts.get_tik_tok_creator_info(
account_id="66b2e19d8c3f5a7e9d0b1c2d",
media_type="video"
)
print(info["privacyLevels"])
```
```bash
curl "https://zernio.com/api/v1/accounts/66b2e19d8c3f5a7e9d0b1c2d/tiktok/creator-info?mediaType=video" \
-H "Authorization: Bearer $ZERNIO_API_KEY"
```
Response (`200`):
```json
{
"creator": { "nickname": "myaccount", "isVerified": false, "canPostMore": true },
"privacyLevels": [
{ "value": "PUBLIC_TO_EVERYONE", "label": "Public To Everyone" },
{ "value": "MUTUAL_FOLLOW_FRIENDS", "label": "Mutual Follow Friends" },
{ "value": "SELF_ONLY", "label": "Self Only" }
],
"postingLimits": {
"maxVideoDurationSec": 600,
"interactionSettings": {
"allow_comment": { "enabled": true, "required": true, "default": false, "label": "Allow Comment" },
"allow_duet": { "enabled": true, "required": true, "default": false, "label": "Allow Duet" },
"allow_stitch": { "enabled": false, "required": true, "default": false, "label": "Allow Stitch" }
}
},
"commercialContentTypes": [
{ "value": "none", "label": "No Commercial Content" },
{ "value": "brand_organic", "label": "Your Brand", "requires": ["is_brand_organic_post"] }
]
}
```
`enabled: false` on a toggle means the creator turned that interaction off in the TikTok app; `required` is always `true` because TikTok forbids defaulting these fields, and `default` is only a value for your composer to render. With `mediaType=photo`, `allow_duet` and `allow_stitch` are `null`.
### Commercial content
`commercialContentType` discloses a commercial post: `"brand_organic"` (the creator's own brand) or `"brand_content"` (a paid partnership). Each value implies its boolean flag, so `isBrandOrganicPost` and `brandPartnerPromote` are only needed to disclose both at once or to override the implied value. Branded content cannot be posted with `privacy_level: "SELF_ONLY"`.
## Commercial Music Library (Business app accounts)
Accounts connected through the TikTok for Business app can attach a Commercial Music Library (CML) track to a post by setting `tiktokSettings.musicSoundInfo`. Use this when you need a specific track (instead of `auto_add_music`).
> **Note:** `musicSoundInfo` is ignored on drafts (`tiktokSettings.draft: true`) because TikTok ignores `post_info` fields in the Creator Inbox flow.
### List trending tracks
Call `GET /v1/accounts/{accountId}/tiktok/commercial-music` to fetch the 100 currently trending CML tracks for the account. The list is not paged; use `countryCode` to select a country chart.
```bash
curl "https://zernio.com/api/v1/accounts/66b2e19d8c3f5a7e9d0b1c2d/tiktok/commercial-music?countryCode=ES" \
-H "Authorization: Bearer $ZERNIO_API_KEY"
```
```javascript
import Zernio from '@zernio/node';
const zernio = new Zernio();
const { data } = await zernio.request({
method: 'GET',
path: '/v1/accounts/66b2e19d8c3f5a7e9d0b1c2d/tiktok/commercial-music',
query: { countryCode: 'ES' }
});
console.log(data.tracks[0]);
```
```python
from zernio import Zernio
client = Zernio()
data = client.request(
method="GET",
path="/v1/accounts/66b2e19d8c3f5a7e9d0b1c2d/tiktok/commercial-music",
query={"countryCode": "ES"},
)
print(data["tracks"][0])
```
Response (`200`):
```json
{
"tracks": [
{
"id": "7212345678901234567",
"commercialMusicId": "7012345678901234567",
"name": "Track Name",
"artist": "Artist Name",
"durationSec": 30,
"genres": ["pop"],
"previewUrl": "https://...",
"thumbnailUrl": "https://...",
"rank": 1,
"clip": {
"id": "7312345678901234567",
"durationSec": 15,
"previewUrl": "https://..."
}
}
]
}
```
### Attach a track to a post
Use `tracks[].id` as `tiktokSettings.musicSoundInfo.musicSoundId` when creating a post (this is the publishable song clip id). `tracks[].clip.id` is also accepted as `musicSoundId` when present. Do not use `commercialMusicId` as `musicSoundId` (TikTok rejects it at publish time). Optionally set track volume and trim points (video posts only).
```bash
curl -X POST https://zernio.com/api/v1/posts \
-H "Authorization: Bearer $ZERNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "New drop with a CML track",
"mediaItems": [
{"type": "video", "url": "https://cdn.example.com/video.mp4"}
],
"platforms": [
{"platform": "tiktok", "accountId": "66b2e19d8c3f5a7e9d0b1c2d"}
],
"tiktokSettings": {
"privacy_level": "PUBLIC_TO_EVERYONE",
"allow_comment": true,
"allow_duet": true,
"allow_stitch": true,
"musicSoundInfo": {
"musicSoundId": "7212345678901234567",
"musicSoundVolume": 50,
"musicSoundStart": 0,
"musicSoundEnd": 15000
},
"videoOriginalSoundVolume": 20,
"content_preview_confirmed": true,
"express_consent_given": true
},
"publishNow": true
}'
```
```javascript
import Zernio from '@zernio/node';
const zernio = new Zernio();
const { data } = await zernio.posts.createPost({
body: {
content: 'New drop with a CML track',
mediaItems: [{ type: 'video', url: 'https://cdn.example.com/video.mp4' }],
platforms: [{ platform: 'tiktok', accountId: '66b2e19d8c3f5a7e9d0b1c2d' }],
tiktokSettings: {
privacy_level: 'PUBLIC_TO_EVERYONE',
allow_comment: true,
allow_duet: true,
allow_stitch: true,
musicSoundInfo: {
musicSoundId: '7212345678901234567',
musicSoundVolume: 50,
musicSoundStart: 0,
musicSoundEnd: 15000
},
videoOriginalSoundVolume: 20,
content_preview_confirmed: true,
express_consent_given: true
},
publishNow: true
}
});
console.log(data.post.platforms[0].status);
```
```python
from zernio import Zernio
client = Zernio()
published = client.posts.create_post(
content="New drop with a CML track",
media_items=[{"type": "video", "url": "https://cdn.example.com/video.mp4"}],
platforms=[{"platform": "tiktok", "accountId": "66b2e19d8c3f5a7e9d0b1c2d"}],
tiktok_settings={
"privacy_level": "PUBLIC_TO_EVERYONE",
"allow_comment": True,
"allow_duet": True,
"allow_stitch": True,
"musicSoundInfo": {
"musicSoundId": "7212345678901234567",
"musicSoundVolume": 50,
"musicSoundStart": 0,
"musicSoundEnd": 15000,
},
"videoOriginalSoundVolume": 20,
"content_preview_confirmed": True,
"express_consent_given": True,
},
publish_now=True,
)
print(published["post"]["platforms"][0]["status"])
```
### Daily posting caps
On an account connected through TikTok's Business app, Zernio allows 15 videos and 15 photo posts per account per rolling 24 hours. The 2 counts are separate, so 15 videos leave the photo allowance untouched. A post beyond the cap is not failed: Zernio holds it and publishes it by itself once the rolling window frees a slot. The window trails the posts that fill it, so there is no clock time at which it resets. Accounts on TikTok's developer app keep Zernio's generic per-platform daily cap instead ([posting velocity limits](/guides/rate-limits#posting-velocity-limits)).
TikTok enforces a limit of its own on how many posts an account creates through the API in 24 hours. A post TikTok rejects for it fails with "You have created too many posts in the last 24 hours via the API"; wait for TikTok's own window to roll, or post in the TikTok app.
### Direct posting at capacity
A TikTok post from an account connected through TikTok's developer app can fail with this message even though nothing is wrong with the account or the media:
```
TikTok direct posting is at capacity right now. Use tiktokSettings.draft: true to deliver via Creator Inbox, or try again in a few hours as capacity frees up.
```
TikTok caps how many distinct accounts can direct-post through one developer app in a rolling 24-hour window (TikTok's own error for it is `reached_active_user_cap`, which Zernio surfaces as `Daily active user quota reached`). Every Zernio customer still on that lane publishes through the same TikTok app, so the budget is app-wide: it is not tied to your account, your API key, or how many posts you have made. An account that has already direct-posted inside the window keeps posting; the cap only blocks accounts that would need a new slot.
Accounts connected through TikTok's Business app do not draw on that budget and never see this error. Reconnecting an account moves it to the Business app, with the caveat in [connection lanes](#connection-lanes).
The window is rolling, so slots free up continuously as activity from 24 hours ago ages out. There is no midnight reset to wait for.
When the window is close to full, Zernio stops handing new developer app slots to accounts whose owner has never added a payment method. Teams with a card on file (or a paid plan) are not gated by Zernio at all, only by TikTok's hard cap, and adding a card moves a team to that group even if the bill stays $0 inside the free allowance. See [Pricing](/pricing).
The post is marked `failed` and is not retried automatically. Either:
- [Retry](/posts/retry-post) it a few hours later, or reschedule it.
- Send it as a draft instead: `tiktokSettings.draft: true` is exempt from the cap (next section).
To check before you create anything, send the same request with `dryRun: true`. It returns `200` with `canPublish` and one verdict per `tiktok` entry, persists no post and claims no slot, so you can repeat it freely. A Business app account comes back `canPublish: true` here whatever the developer app budget looks like, because the cap does not apply to it.
### Draft delivery
Set `tiktokSettings.draft: true` and Zernio uploads the media to the creator's TikTok inbox instead of publishing it. Videos go through TikTok's inbox upload endpoint; photo posts go through the content endpoint with `post_mode: MEDIA_UPLOAD`. Drafts need the `video.upload` scope (granted in the standard connect flow) and TikTok app version 31.8 or later on the creator's phone.
The creator gets an inbox notification in the TikTok app and finishes the post in TikTok's own editor: caption, cover, privacy, and the final Post tap all happen there. Nothing is public until the creator posts it.
Zernio marks the platform entry `published` as soon as TikTok accepts the upload, which is the moment the draft is handed over, not the moment it goes live. The entry carries `platformSpecificData.isDraft: true` and no `platformPostUrl`, and the `post.tiktok.url_resolved` webhook never fires for drafts, because Zernio never learns whether or when the creator posts it.
Limits that still apply:
- TikTok allows 5 pending drafts per account in any 24-hour period. A 6th fails with `TikTok allows only 5 pending drafts per account in any 24-hour period...` and the only fix is to finish or discard drafts in the TikTok app; waiting does not free the slot.
- TikTok's own per-account limit on posts created through the API in the last 24 hours (`You have created too many posts in the last 24 hours via the API`) and Zernio's own caps (25 posts per hour, plus the [daily posting caps](#daily-posting-caps)) count drafts and direct posts alike.
- Drafts are exempt from the [direct posting capacity](#direct-posting-at-capacity) gate.
## Platform fields
All fields go in `tiktokSettings` at the top level of the request. Names are shown in `snake_case`; `camelCase` is accepted too.
Zernio merges that object into each TikTok entry's `platformSpecificData.tiktokSettings`, and a key set there wins over the root-level one. Use the per-entry object when 2 TikTok accounts in the same request need different settings, for example one publishing directly and one with `draft: true`.
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `privacy_level` | string | | Required. One of the creator's values from [creator info](#creator-info): `PUBLIC_TO_EVERYONE`, `MUTUAL_FOLLOW_FRIENDS`, `FOLLOWER_OF_CREATOR`, `SELF_ONLY`. Accounts connected through TikTok's Business app publish videos as public only: non-public values on video direct posts are rejected unless `draft: true` (photo posts keep every level). See [connection lanes](#connection-lanes). |
| `allow_comment` | boolean | | Required. Enable or disable comments on the post. |
| `allow_duet` | boolean | | Required for videos. Enable or disable duets. |
| `allow_stitch` | boolean | | Required for videos. Enable or disable stitches. |
| `content_preview_confirmed` | boolean | | Required, must be `true`. Legal requirement from TikTok. |
| `express_consent_given` | boolean | | Required, must be `true`. Legal requirement from TikTok. |
| `video_cover_timestamp_ms` | number | `1000` | Cover frame position in milliseconds. Ignored when `video_cover_image_url` is set. |
| `video_cover_image_url` | string (URL) | | Custom cover image (JPG, PNG or WebP, at most 20 MB). Overrides `video_cover_timestamp_ms`. |
| `media_type` | `"video"` \| `"photo"` | (from media) | Set to `"photo"` for photo carousels. |
| `photo_cover_index` | number | `0` | Which image is the cover (0-based). |
| `description` | string | | Long-form caption for photo carousels, up to 4,000 characters. |
| `auto_add_music` | boolean | | Let TikTok add recommended music. Photo carousels only. With the brand-organic or branded-content toggle on, TikTok allows Commercial Music Library tracks only, so this attaches nothing there; use `musicSoundInfo` instead. |
| `musicSoundInfo` | object | | Commercial Music Library track to attach (TikTok for Business app connections only). Pick `musicSoundId` from `GET /v1/accounts/{accountId}/tiktok/commercial-music`. Ignored on drafts. |
| `musicSoundInfo.musicSoundId` | string | | The `id` field of a track from `GET /v1/accounts/{accountId}/tiktok/commercial-music` (a song clip id). TikTok rejects the commercial music id at publish time. |
| `musicSoundInfo.musicSoundVolume` | number | `50` | Track volume (0 to 100). Video posts only. |
| `musicSoundInfo.musicSoundStart` | number | `0` | Start point in milliseconds. Video posts only. |
| `musicSoundInfo.musicSoundEnd` | number | | End point in milliseconds (defaults to video length). Must be greater than `musicSoundStart`. Video posts only. |
| `videoOriginalSoundVolume` | number | | Volume of the video's own sound when a commercial track is attached (0 to 100). Requires `musicSoundInfo`. Video posts only. |
| `video_made_with_ai` | boolean | | AI-generated content disclosure. Accounts connected through TikTok's Business app carry the disclosure on video posts only: direct photo posts reject `true`; use `draft: true` and set the disclosure in the TikTok app. |
| `draft` | boolean | `false` | Send to the Creator Inbox instead of publishing. See [Draft delivery](#draft-delivery). |
| `commercialContentType` | `"none"` \| `"brand_organic"` \| `"brand_content"` | | Commercial content disclosure. See [Commercial content](#commercial-content). |
| `isBrandOrganicPost` | boolean | | Implied by `commercialContentType: "brand_organic"`; set it only to disclose both types at once. |
| `brandPartnerPromote` | boolean | | Implied by `commercialContentType: "brand_content"`; set it only to disclose both types at once. |
## Media requirements
Videos are uploaded in chunks of 5 to 64 MB. A post holds either one video or up to 35 photos, never both.
### Images
| Property | Requirement |
|----------|-------------|
| Max photos | 35 per carousel |
| Formats | JPEG, PNG, WebP |
| Max file size | 20 MB per image |
| Aspect ratio | 9:16 recommended |
| Resolution | Downscaled to fit inside 1080 x 1920 px, aspect ratio kept; an image already inside those bounds is sent untouched |
### Videos
| Property | Requirement |
|----------|-------------|
| Max videos | 1 per post |
| Formats | MP4, MOV, WebM |
| Max file size | 4 GB |
| Max duration | 10 minutes |
| Min duration | 3 seconds |
| Aspect ratio | 9:16 vertical (the only format that works well) |
| Resolution | 1080 x 1920 px recommended |
| Codec | H.264 |
| Frame rate | 30 fps recommended |
### Media URLs
Google Drive, Dropbox, OneDrive, SharePoint and iCloud links return an HTML page, not the file, so TikTok cannot download from them. A media URL must be public with no authentication, return the media bytes with the correct `Content-Type`, not redirect to an HTML page, and sit on a fast host; test it in an incognito window. Or upload through the [media endpoint](/guides/media-uploads).
## Analytics
Call `GET /v1/analytics?platform=tiktok` ([Analytics API](/analytics/get-analytics)).
| Metric | Available |
|--------|-----------|
| Likes | |
| Comments | |
| Shares | |
| Views | |
| Profile views | (Business app connections) |
| Completion rate | (Business app connections) |
```typescript
const { data: analytics } = await zernio.analytics.getAnalytics({
query: { platform: 'tiktok', fromDate: '2026-08-01', toDate: '2026-08-31' }
});
console.log(analytics.posts);
```
```python
analytics = client.analytics.get_analytics(
platform="tiktok",
from_date="2026-08-01",
to_date="2026-08-31"
)
print(analytics["posts"])
```
```bash
curl "https://zernio.com/api/v1/analytics?platform=tiktok&fromDate=2026-08-01&toDate=2026-08-31" \
-H "Authorization: Bearer $ZERNIO_API_KEY"
```
Response (`200`), one entry per post:
```json
{
"posts": [
{
"_id": "65f1c0a9e2b5af0012ab34cd",
"platform": "tiktok",
"status": "published",
"publishedAt": "2026-08-14T10:00:05Z",
"platformPostUrl": "https://www.tiktok.com/@myaccount/video/7300000000000000000",
"analytics": {
"likes": 342,
"comments": 28,
"shares": 45,
"views": 15420
}
}
]
}
```
[Account insights](/analytics/get-tiktok-account-insights) adds account-level counters (`follower_count`, `following_count`, `likes_count`, `video_count`) plus `followers_gained` and `followers_lost` deltas. Live values come from the `user.info.stats` scope; the historical series is joined from Zernio's daily snapshotter.
Accounts connected through TikTok's Business app also report `profileViews`, the profile views a post brought in, and `completionRate`, the share of viewers who watched to the end as a number from 0 to 1. Both land 24 to 48 hours after publishing, and TikTok reports completion only for posts active in the last 7 days. The remaining TikTok Studio metrics (account-level impressions and reach, per-video watch time and average watch time, impression sources such as For You, Following, Hashtag and Search) are not available on any public TikTok API. TikTok's Research API does not expose them either and is restricted to non-commercial academic use under TikTok's eligibility policy.
## Inbox
TikTok supports DMs and comments for accounts connected through TikTok's Business app, which every new TikTok connection uses. An account still connected through TikTok's developer app gets a `400` with code `PLATFORM_LIMITATION` for both.
### Direct messages
TikTok decides which accounts can use DMs:
- The account is a TikTok Business Account. TikTok refuses personal accounts.
- The account's sign-up region is outside the EEA, Switzerland and the UK.
- The account accepts DMs, from everyone or by accepting message requests. Otherwise TikTok sends no webhook for incoming messages.
You can only reply: TikTok does not let a business start a conversation. After the user's last message, TikTok accepts up to 10 messages within 48 hours, and a send outside that window fails with TikTok's error.
| Feature | Supported |
|---------|-----------|
| List conversations | |
| Fetch messages | |
| Send text messages | (up to 6,000 characters) |
| Send an image | (one JPG or PNG, up to 3 MB) |
| Start a conversation | |
Reply with [Send message](/messages/send-inbox-message) (`POST /v1/inbox/conversations/{conversationId}/messages`). A message is text or one image, never both: a request with `message` and an attachment returns `400` with code `PLATFORM_LIMITATION`.
### Comments
Comment management does not need a Business Account.
- Read comments: `GET /v1/inbox/comments/{postId}` (TikTok video id). Each top-level comment includes up to three inline replies; pass `commentId` to page the full reply list for one comment.
- Post a comment, reply, and like, hide or unhide any comment. Delete works on your own comments and replies.
- Pin and unpin a top-level comment, as shown below.
### Pin and unpin comments
Pin a top-level comment:
```bash
curl -X POST https://zernio.com/api/v1/inbox/comments/{postId}/{commentId}/pin \
-H "Authorization: Bearer $ZERNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"accountId": "66b2e19d8c3f5a7e9d0b1c2d"
}'
```
```javascript
import Zernio from '@zernio/node';
const zernio = new Zernio();
const { data } = await zernio.request({
method: 'POST',
path: `/v1/inbox/comments/${postId}/${commentId}/pin`,
body: { accountId: '66b2e19d8c3f5a7e9d0b1c2d' }
});
console.log(data);
```
```python
from zernio import Zernio
client = Zernio()
data = client.request(
method="POST",
path=f"/v1/inbox/comments/{post_id}/{comment_id}/pin",
body={"accountId": "66b2e19d8c3f5a7e9d0b1c2d"},
)
print(data)
```
Unpin a previously pinned comment:
```bash
curl -X DELETE "https://zernio.com/api/v1/inbox/comments/{postId}/{commentId}/pin?accountId=66b2e19d8c3f5a7e9d0b1c2d" \
-H "Authorization: Bearer $ZERNIO_API_KEY"
```
```javascript
import Zernio from '@zernio/node';
const zernio = new Zernio();
const { data } = await zernio.request({
method: 'DELETE',
path: `/v1/inbox/comments/${postId}/${commentId}/pin`,
query: { accountId: '66b2e19d8c3f5a7e9d0b1c2d' }
});
console.log(data);
```
```python
from zernio import Zernio
client = Zernio()
data = client.request(
method="DELETE",
path=f"/v1/inbox/comments/{post_id}/{comment_id}/pin",
query={"accountId": "66b2e19d8c3f5a7e9d0b1c2d"},
)
print(data)
```
### Webhooks
TikTok supports `comment.received`, `message.received` and `message.sent` webhooks. Comment events include only the author id (no username, picture, or owner flag).
## What you cannot do
TikTok's API does not expose:
- TikTok's sound and music library for general search/browse (Zernio exposes trending Commercial Music Library tracks for Business app connections via `GET /v1/accounts/{accountId}/tiktok/commercial-music`)
- Creating duets or stitches
- Going live
- Effects or filters
- Editing posts after publishing
- For You Page analytics
- Playlists
- Starting a DM conversation (you can only reply)
- Text-only posts (media required)
## Common errors
| Error | Cause | Fix |
|-------|-------|-----|
| "You have created too many posts in the last 24 hours via the API." | TikTok's daily API posting limit for the account | Wait for the rolling 24-hour window or post in the TikTok app. |
| "TikTok direct posting is at capacity right now." / "Daily active user quota reached" | TikTok's app-wide cap on distinct posting accounts per rolling 24 hours, on developer app connections only | Retry a few hours later, send as a draft, or reconnect the account to TikTok's Business app, which is exempt. See [Direct posting at capacity](#direct-posting-at-capacity). |
| "Publishing failed during platform API call (timeout waiting for platform response)" | TikTok took too long to process the upload | Normal for large videos. Check the post status after a few minutes. |
| "Selected privacy level 'X' is not available for this creator. Available options: ..." | `privacy_level` is not in the creator's allowed values | Read [creator info](#creator-info) and use one of its `privacyLevels`. |
| "TikTok flagged this post as potentially risky (spam_risk)" | Content moderation flagged the post | Review the content. TikTok's API moderation is stricter than the app. |
| "Duplicate content detected." | The same content was posted recently | Change the caption or media before retrying. |
| "TikTok video upload failed: Your video URL returned an error (download failed)" | TikTok could not download the video from the URL | Use a direct download URL, not a cloud storage sharing page. |
| "Missing required TikTok permissions. Please reconnect with all required scopes." | The OAuth token lacks a required scope | Reconnect the TikTok account and grant every scope. An authorization that leaves one out returns `missing_tiktok_permissions` and changes nothing ([connection lanes](#connection-lanes)). |
A `publishNow: true` post that TikTok rejects returns `207` with `post.status: "failed"` and the message in `platforms[].errorMessage`:
```json
{
"message": "Post created but publishing failed",
"error": "All platforms failed",
"post": {
"_id": "65f1c0a9e2b5af0012ab34cd",
"status": "failed",
"platforms": [
{
"platform": "tiktok",
"status": "failed",
"errorMessage": "TikTok direct posting is at capacity right now. Use tiktokSettings.draft: true to deliver via Creator Inbox, or try again in a few hours as capacity frees up."
}
]
}
}
```
`207` is a 2xx status, so `fetch(...).ok` is `true`; branch on the status code and on `post.status`. [Error handling](/guides/error-handling) covers the envelope.
## Related
- [Connecting accounts](/guides/connecting-accounts): the OAuth flow.
- [Create post](/posts/create-post): every field of the request.
- [Media uploads](/guides/media-uploads): upload videos and photos instead of hosting them.
- [Analytics](/analytics/get-analytics) and [Account insights](/analytics/get-tiktok-account-insights): post and account metrics.
- [Rate limits](/guides/rate-limits#tiktok-caps-direct-posting-per-application): how Zernio handles TikTok's quotas.
- [Pricing](/pricing): what a connected account and analytics cost.
---
# X Ads
Create campaigns, promote posts you already published and upload Tailored Audiences on an xads account, with OAuth 1.0a signing handled server-side.
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
Create a campaign with `POST /v1/ads/create`, or promote a post you already published with `POST /v1/ads/boost`, on an `xads` account for X (platform value `twitter`). Zernio signs every X Ads call with OAuth 1.0a on its own servers, so your integration sends `$ZERNIO_API_KEY` the same way it does for every other platform.
## Quick reference
| Property | Value |
|----------|-------|
| Hierarchy | Campaign > Line Item > Promoted Tweet, created in one call |
| Goals (`goal`) | `engagement`, `traffic`, `awareness`, `video_views`, `app_promotion` |
| Ad text | `body`, 280 characters including the shortened link |
| Ignored creative fields | `headline`, `imageUrl`, `callToAction` |
| Media | None on a create (the ad is a text post); a boost carries the media of the post it promotes |
| Targeting | Country, postal code, language |
| Audiences | Tailored Audiences (create, member upload, read) |
| Analytics | Yes |
| Edits after creation | Status and budget only |
## Before you start
X Ads is a separate OAuth from X posting, so the profile needs both: a connected X posting account to author the ads, and the `xads` account created from it. There is no ads-only mode. You also need the X Ads account id, a base36 string such as `18ce54d4x5t`, from [List ad accounts](/ad-accounts/list-ad-accounts).
`accountId` takes either shape. Pass the X posting account id or the `xads` account id and Zernio resolves the sibling internally. When the resolved profile has no X Ads connection, the call returns `422` with code `ads_connection_required`.
## Connect
Call `GET /v1/connect/twitter/ads` with `profileId` and the `accountId` of the X posting account the ads are authored by ([Connect ads](/connect/connect-ads)). `accountId` is required here: X ads carry a real author, so there is no ads-only path.
```bash
curl "https://zernio.com/api/v1/connect/twitter/ads?profileId=66a1f0c2a4b9d3e8f1a2b3c4&accountId=66b2e19d8c3f5a7e9d0b1c2d" \
-H "Authorization: Bearer $ZERNIO_API_KEY"
```
Response (`200`):
```json
{
"authUrl": "https://api.x.com/oauth/authorize?oauth_token=...",
"state": "..."
}
```
Send the user to `authUrl`, as in the [connecting accounts guide](/guides/connecting-accounts). X Ads keeps its own token, so this round trip always happens the first time; afterwards the same call returns `alreadyConnected: true` with the `xads` `accountId`. On failure the browser lands on your `redirect_url` with `platform=xads` and an `error` slug such as `x_ads_denied`, `x_ads_auth_failed` or `x_ads_token_failed`. Pass `adAccountId` on the connect call to scope ad sync to one X Ads account, or `adAccountIds` (repeated or comma-separated) for several. Sending neither leaves whatever scope is already stored; sending one removes ads already synced from the accounts it drops.
## Create a campaign
Call `POST /v1/ads/create`. On X, `body` carries the ad text, at most 280 characters, and X counts a `linkUrl` as about 24 of them because it shortens the link. `headline`, `imageUrl` and `callToAction` have no slot on X and are ignored: the ad X publishes is a text post built from `body` and `linkUrl`. To run an X ad with an image or a video, [promote a post](#promote-an-existing-post) that already carries it, within [X's media limits](/platforms/twitter/media).
```typescript
import Zernio from '@zernio/node';
const zernio = new Zernio();
const { data: created } = await zernio.adcampaigns.createStandaloneAd({
body: {
accountId: '66b2e19d8c3f5a7e9d0b1c2d',
adAccountId: '18ce54d4x5t',
name: 'Q2 product awareness',
goal: 'awareness',
budgetAmount: 75,
budgetType: 'daily',
body: 'Ship faster with platform engineering done right.',
linkUrl: 'https://example.com/platform',
countries: ['US']
}
});
console.log(created.ad.platformAdSetId);
```
```python
from zernio import Zernio
client = Zernio()
created = client.ad_campaigns.create_standalone_ad(
account_id="66b2e19d8c3f5a7e9d0b1c2d",
ad_account_id="18ce54d4x5t",
name="Q2 product awareness",
goal="awareness",
budget_amount=75,
budget_type="daily",
body="Ship faster with platform engineering done right.",
link_url="https://example.com/platform",
countries=["US"],
)
print(created["ad"]["platformAdSetId"])
```
```bash
curl -X POST "https://zernio.com/api/v1/ads/create" \
-H "Authorization: Bearer $ZERNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"adAccountId": "18ce54d4x5t",
"name": "Q2 product awareness",
"goal": "awareness",
"budgetAmount": 75,
"budgetType": "daily",
"body": "Ship faster with platform engineering done right.",
"linkUrl": "https://example.com/platform",
"countries": ["US"]
}'
```
Response (`201`):
```json
{
"ad": {
"_id": "66f0a1b2c3d4e5f6a7b8c9d0",
"name": "Q2 product awareness",
"platform": "twitter",
"status": "pending_review",
"adType": "standalone",
"goal": "awareness",
"budget": { "amount": 75, "type": "daily" },
"platformAdId": "5gzhb",
"platformCampaignId": "f4x8j",
"platformAdSetId": "9r2mv"
},
"message": "Ad created"
}
```
`budgetAmount` is in whole currency units of the ad account, so `75` is $75.00 on a USD account. `platformAdSetId` is the X line item. Every sample below reuses the `zernio` and `client` constructors from this one.
### Targeting fields
`countries`, `languages` and `zips` apply on X. `incomeTier` is rejected, and `cities`, `regions`, `metros`, `customLocations` and `behaviors` are Meta and TikTok only. `audienceInclude` and `audienceExclude` take the X custom audience id of a Tailored Audience.
## Promote an existing post
`POST /v1/ads/boost` promotes a post that is already published, keeping its engagement.
```typescript
const { data: boosted } = await zernio.adcampaigns.boostPost({
body: {
postId: '65f1c0a9e2b5af0012ab34cd',
accountId: '66b2e19d8c3f5a7e9d0b1c2d',
adAccountId: '18ce54d4x5t',
name: 'Boost launch post',
goal: 'engagement',
budget: { amount: 50, type: 'daily' },
schedule: { startDate: '2027-01-04', endDate: '2027-01-11' }
}
});
console.log(boosted.ad._id);
```
```python
boosted = client.ad_campaigns.boost_post(
post_id="65f1c0a9e2b5af0012ab34cd",
account_id="66b2e19d8c3f5a7e9d0b1c2d",
ad_account_id="18ce54d4x5t",
name="Boost launch post",
goal="engagement",
budget={"amount": 50, "type": "daily"},
schedule={"startDate": "2027-01-04", "endDate": "2027-01-11"},
)
print(boosted["ad"]["_id"])
```
```bash
curl -X POST "https://zernio.com/api/v1/ads/boost" \
-H "Authorization: Bearer $ZERNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"postId": "65f1c0a9e2b5af0012ab34cd",
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"adAccountId": "18ce54d4x5t",
"name": "Boost launch post",
"goal": "engagement",
"budget": { "amount": 50, "type": "daily" },
"schedule": { "startDate": "2027-01-04", "endDate": "2027-01-11" }
}'
```
Response (`201`):
```json
{
"ad": {
"_id": "66f0a1b2c3d4e5f6a7b8c9d1",
"name": "Boost launch post",
"platform": "twitter",
"status": "pending_review",
"adType": "boost",
"goal": "engagement",
"budget": { "amount": 50, "type": "daily" }
},
"message": "Ad created"
}
```
Boosts are not idempotent. Send an `Idempotency-Key` header to make a retry replay the original `201` instead of creating a second ad ([idempotency](/guides/idempotency)).
## Tailored Audiences
Create the audience with `POST /v1/ads/audiences` (`type: "customer_list"`), then upload members with `POST /v1/ads/audiences/{audienceId}/users`. X matches on email, ignores `phone`, and takes at most 10,000 users per request; Zernio hashes every value with SHA-256 before it leaves.
```typescript
const { data: audience } = await zernio.adaudiences.createAdAudience({
body: {
accountId: '66b2e19d8c3f5a7e9d0b1c2d',
adAccountId: '18ce54d4x5t',
type: 'customer_list',
name: 'Trial signups'
}
});
const { data: upload } = await zernio.adaudiences.addUsersToAdAudience({
path: { audienceId: audience.audience.id },
body: { users: [{ email: 'jane@example.com' }, { email: 'sam@example.com' }] }
});
console.log(upload.numReceived);
```
```python
audience = client.ad_audiences.create_ad_audience(
account_id="66b2e19d8c3f5a7e9d0b1c2d",
ad_account_id="18ce54d4x5t",
type="customer_list",
name="Trial signups",
)
upload = client.ad_audiences.add_users_to_ad_audience(
audience_id=audience["audience"]["id"],
users=[{"email": "jane@example.com"}, {"email": "sam@example.com"}],
)
print(upload["numReceived"])
```
```bash
curl -X POST "https://zernio.com/api/v1/ads/audiences" \
-H "Authorization: Bearer $ZERNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "accountId": "66b2e19d8c3f5a7e9d0b1c2d", "adAccountId": "18ce54d4x5t", "type": "customer_list", "name": "Trial signups" }'
curl -X POST "https://zernio.com/api/v1/ads/audiences/66e5b2c3d4f5a6b7c8d9e0f1/users" \
-H "Authorization: Bearer $ZERNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "users": [{ "email": "jane@example.com" }, { "email": "sam@example.com" }] }'
```
Response (`201`):
```json
{
"audience": { "id": "66e5b2c3d4f5a6b7c8d9e0f1", "name": "Trial signups", "type": "customer_list", "platform": "twitter", "status": "pending" },
"message": "Audience created"
}
```
Response (`200`):
```json
{ "message": "Users added", "numReceived": 2, "numInvalid": 0 }
```
An audience must match at least 100 recently active users before X allows targeting against it, so its size reads as 0 until then.
## Analytics
Call `GET /v1/ads/{adId}/analytics` with the ad's `_id` for spend, impressions, clicks, CTR, CPC and CPM over a date range ([Get ad analytics](/ad-insights/get-ad-analytics)).
```bash
curl "https://zernio.com/api/v1/ads/66f0a1b2c3d4e5f6a7b8c9d0/analytics?fromDate=2027-01-04&toDate=2027-01-11" \
-H "Authorization: Bearer $ZERNIO_API_KEY"
```
Response (`200`), trimmed:
```json
{
"ad": { "id": "66f0a1b2c3d4e5f6a7b8c9d0", "name": "Q2 product awareness", "platform": "twitter", "status": "active", "currency": "USD" },
"analytics": {
"summary": { "spend": 421.7, "impressions": 203800, "clicks": 3140, "ctr": 1.54, "cpc": 0.13, "cpm": 2.07, "engagement": 5820 },
"daily": [ { "date": "2027-01-04", "spend": 59.8, "impressions": 29100, "clicks": 448 } ]
}
}
```
X reports `reach` as 0, because Zernio does not sync it, and the demographic `breakdowns` parameter is Meta and TikTok only. `GET /v1/ads/tree` rolls the same metrics up per campaign and line item ([Get campaign tree](/ad-campaigns/get-ad-tree)).
## What you cannot do
X Ads through Zernio does not support:
- Targeting or creative edits after creation. `PUT /v1/ads/{adId}` takes status and budget; `targeting` or `creative` returns `501` with code `unsupported_platform_operation`.
- Campaign duplication. `POST /v1/ads/campaigns/{campaignId}/duplicate` returns `501` on X.
- An ads-only connection. Every X ad is authored by a connected posting account.
- Keyword targeting and follower look-alike targeting.
- Uploading creative with an ad. A create publishes text; media comes from the post a boost promotes.
- Income tier targeting, and city, region, metro or radius targeting.
## Common errors
| Error | Cause | Fix |
|-------|-------|-----|
| `422` with code `ads_connection_required` | The profile has no `xads` account | Run the [connect](#connect) flow with the X posting account id. |
| `400` with code `missing_required_field` | `body` or `linkUrl` is absent on a create | Send the ad text and its destination. |
| `400` with code `invalid_field_value` | A `goal` outside the 5 X values, or ad text over 280 characters | Use a supported goal and trim the text, counting about 24 characters for the link. |
| `403` with code `ads_allowance_exceeded` | The team has no payment method on file and has reached 500 live ads | Add a card to resume creating ads. |
| `502` with code `platform_api_error` | X rejected the request | Read `platformError` for X's own payload. |
A call against a profile with no X Ads connection returns:
```json
{
"error": "X Ads is not connected for this profile",
"type": "invalid_request_error",
"code": "ads_connection_required",
"param": "accountId"
}
```
[Error handling](/guides/error-handling) covers the envelope and the stable codes.
## Related
- [X](/platforms/twitter): the posting account that authors the ads.
- [Connecting accounts](/guides/connecting-accounts): the OAuth flow behind `authUrl`.
- [Create standalone ad](/ad-campaigns/create-standalone-ad) and [Boost post](/ad-campaigns/boost-post): every field of both requests.
- [Create ad audience](/ad-audiences/create-ad-audience) and [Add users to ad audience](/ad-audiences/add-users-to-ad-audience).
- [Get ad analytics](/ad-insights/get-ad-analytics): metrics, date ranges and rollups.
---
# YouTube
Publish videos and Shorts to YouTube with the Zernio API, with custom thumbnails, visibility, playlists, COPPA and AI disclosure flags, and description edits after publishing.
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
Publish videos and Shorts to YouTube with `POST /v1/posts` and `platform: "youtube"`. The same account serves analytics and comments.
## Quick reference
| Property | Value |
|----------|-------|
| Title limit | 100 characters |
| Description limit | 5,000 characters |
| Tags | Top-level `tags` array on the create request; 100 characters per tag, 500 combined |
| Videos per post | 1 |
| Video formats | MP4, MOV, AVI, WMV, FLV, 3GP, WebM |
| Video max size | 256 GB |
| Video max duration | 15 minutes (unverified channel), 12 hours (verified) |
| Thumbnail formats | JPEG, PNG, GIF |
| Thumbnail max size | 2 MB |
| Post types | Video, Shorts |
| Scheduling | Yes (a public video is uploaded early as private and released by YouTube at the scheduled time) |
| Editing published posts | Yes (description only; title and tags through a separate endpoint) |
| Inbox (comments) | Yes |
| Inbox (DMs) | No (YouTube has no DM system) |
| Analytics | Yes |
## Before you start
YouTube requires a channel owned by the Google identity you authorize, on a personal Google account or a Brand Account; see [Brand Accounts and multiple channels](#brand-accounts-and-multiple-channels). Every post is exactly one video, so there are no image-only or text-only posts. Unverified channels are limited to 15-minute videos; verify the channel by phone at [youtube.com/verify](https://www.youtube.com/verify) for longer uploads. Daily upload quotas vary by channel, and Shorts are detected from duration and aspect ratio, not chosen with a flag.
If a channel is suspended, every upload fails with a `403`. Call [Account health](/accounts/get-all-accounts-health) before scheduling posts to a channel you do not control.
## Connect
Call `GET /v1/connect/youtube` with `profileId` on [Get OAuth connect URL](/connect/get-connect-url). The [connecting accounts guide](/guides/connecting-accounts) covers the OAuth flow and [scopes](/guides/connecting-accounts#scopes) in general; [Account health](/accounts/get-all-accounts-health) reports what a connected account can do with the scopes the user granted.
### OAuth scopes
| Scope | What it enables |
|-------|-----------------|
| `https://www.googleapis.com/auth/youtube.upload` | Upload videos to the channel |
| `https://www.googleapis.com/auth/youtube` | Manage the channel: video metadata, playlists, thumbnails |
| `https://www.googleapis.com/auth/youtube.force-ssl` | Read and post comments |
| `https://www.googleapis.com/auth/yt-analytics.readonly` | Channel and video analytics |
### Brand Accounts and multiple channels
There is no channel picker. YouTube connects straight after Google's OAuth screen, and the channel Zernio connects is the one owned by the Google identity you pick in Google's account chooser. Zernio forces that chooser on every connect (`prompt=select_account`), so the choice is always yours to make:
- A channel on your personal Google account: pick your personal identity.
- A channel that lives on a Brand Account: pick the Brand Account entry in the chooser, not your personal identity. You need owner or manager access to the Brand Account at [account.google.com/brandaccounts](https://account.google.com/brandaccounts).
- YouTube Studio "Manage access" permissions do not grant API access. Someone added as an editor or manager only inside YouTube Studio cannot connect that channel; a Brand Account owner or manager has to do it.
If the identity you picked owns no channel (personal identity chosen by mistake, Studio-only access, or no channel created yet), the connect fails with `We couldn't find a YouTube channel for the Google account you authorized...`. Restart the flow and pick the right entry in the chooser.
A profile holds one YouTube channel. To connect another one, create a second [profile](/guides/profiles), select it, start the YouTube connect again, and pick the other channel's identity in the chooser. Each channel then has its own `accountId`.
## Publish
A plain post becomes a public video whose title is the first line of `content` and whose description is the whole of `content`. YouTube classifies it as a Short on its own when it is 3 minutes or shorter and vertical.
### Video
Long-form content: longer than 3 minutes or horizontal. 16:9 is the recommended aspect ratio, and a `thumbnail` on the media item sets the custom cover.
```typescript
import Zernio from '@zernio/node';
const zernio = new Zernio();
const { data: published } = await zernio.posts.createPost({
body: {
content: 'In this tutorial, I walk through building a REST API from scratch.\n\n#programming #tutorial',
tags: ['rest api', 'node.js', 'backend tutorial'],
mediaItems: [{
type: 'video',
url: 'https://cdn.example.com/long-form-video.mp4',
thumbnail: 'https://cdn.example.com/thumbnail.jpg'
}],
platforms: [{
platform: 'youtube',
accountId: '66b2e19d8c3f5a7e9d0b1c2d',
platformSpecificData: {
title: 'Build a REST API from scratch',
visibility: 'public',
categoryId: '27',
madeForKids: false
}
}],
publishNow: true
}
});
console.log(published.post.platforms[0].platformPostUrl);
```
```python
from zernio import Zernio
client = Zernio()
published = client.posts.create_post(
content="In this tutorial, I walk through building a REST API from scratch.\n\n#programming #tutorial",
tags=["rest api", "node.js", "backend tutorial"],
media_items=[{
"type": "video",
"url": "https://cdn.example.com/long-form-video.mp4",
"thumbnail": "https://cdn.example.com/thumbnail.jpg"
}],
platforms=[{
"platform": "youtube",
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"platformSpecificData": {
"title": "Build a REST API from scratch",
"visibility": "public",
"categoryId": "27",
"madeForKids": False
}
}],
publish_now=True
)
print(published["post"]["platforms"][0]["platformPostUrl"])
```
```bash
curl -X POST https://zernio.com/api/v1/posts \
-H "Authorization: Bearer $ZERNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "In this tutorial, I walk through building a REST API from scratch.\n\n#programming #tutorial",
"tags": ["rest api", "node.js", "backend tutorial"],
"mediaItems": [{
"type": "video",
"url": "https://cdn.example.com/long-form-video.mp4",
"thumbnail": "https://cdn.example.com/thumbnail.jpg"
}],
"platforms": [{
"platform": "youtube",
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"platformSpecificData": {
"title": "Build a REST API from scratch",
"visibility": "public",
"categoryId": "27",
"madeForKids": false
}
}],
"publishNow": true
}'
```
Response (`201`):
```json
{
"post": {
"_id": "65f1c0a9e2b5af0012ab34cd",
"status": "published",
"platforms": [
{
"platform": "youtube",
"status": "published",
"platformPostUrl": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
}
]
}
}
```
`tags` sits at the top level of the request, alongside `content`, and reaches YouTube as `snippet.tags` on the upload. Zernio strips a leading `#`, splits a comma-joined entry into separate tags and drops duplicates, then keeps tags in order until the combined length would pass 500 characters. A single tag longer than 100 characters is skipped.
Every sample below changes only the `platforms` entry of this request.
### Shorts
YouTube detects Shorts on its own: a video that is 3 minutes or shorter and vertical (9:16) is classified as a Short. There is no post type or flag to set, so the request is the one above with a short vertical video. Videos under 15 seconds loop automatically, and custom thumbnails are not supported for Shorts through the API.
### Playlists
Create a playlist with `POST /v1/accounts/{accountId}/youtube-playlists` ([Create YouTube playlist](/connect/create-youtube-playlist)). Only `title` is required; `privacy` defaults to `private` and also accepts `public` or `unlisted`.
```typescript
const { data: created } = await zernio.connect.createYoutubePlaylist({
path: { accountId: '66b2e19d8c3f5a7e9d0b1c2d' },
body: { title: 'Tutorials', description: 'Step-by-step video tutorials', privacy: 'private' }
});
console.log(created.playlist.id);
```
```python
created = client.connect.create_youtube_playlist(
account_id="66b2e19d8c3f5a7e9d0b1c2d",
title="Tutorials",
description="Step-by-step video tutorials",
privacy="private"
)
print(created["playlist"]["id"])
```
```bash
curl -X POST "https://zernio.com/api/v1/accounts/66b2e19d8c3f5a7e9d0b1c2d/youtube-playlists" \
-H "Authorization: Bearer $ZERNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"title":"Tutorials","description":"Step-by-step video tutorials","privacy":"private"}'
```
Response (`201`):
```json
{
"playlist": {
"id": "PLxxxxxxxxxxxxx",
"title": "Tutorials",
"description": "Step-by-step video tutorials",
"privacy": "private",
"itemCount": 0,
"thumbnailUrl": ""
}
}
```
Creation costs 50 YouTube quota units and requires the `youtube` or `youtube.force-ssl` OAuth scope. The playlist starts empty and does not become the account's default. Use the returned `playlist.id` in `platformSpecificData.playlistId` when publishing a video.
Playlist creation is not idempotent. Repeating a request can create another playlist, including after a timeout. Zernio does not automatically retry creation; list the channel's playlists before retrying an ambiguous failure.
YouTube's public API does not expose the **official series** setting. After creating a playlist, enable **Set as official series for this playlist** manually in YouTube's desktop playlist settings. See [YouTube's series playlist requirements](https://support.google.com/youtube/answer/6084043).
`playlistId` adds the video to an existing playlist after upload, for immediate and scheduled uploads alike. Without it the video is uploaded normally. List the channel's playlists with `GET /v1/accounts/{accountId}/youtube-playlists` ([List YouTube playlists](/connect/get-youtube-playlists)):
```typescript
const { data: playlists } = await zernio.connect.getYoutubePlaylists({
path: { accountId: '66b2e19d8c3f5a7e9d0b1c2d' }
});
console.log(playlists.playlists);
```
```python
playlists = client.connect.get_youtube_playlists(
account_id="66b2e19d8c3f5a7e9d0b1c2d"
)
print(playlists["playlists"])
```
```bash
curl "https://zernio.com/api/v1/accounts/66b2e19d8c3f5a7e9d0b1c2d/youtube-playlists" \
-H "Authorization: Bearer $ZERNIO_API_KEY"
```
Response (`200`):
```json
{
"playlists": [
{
"id": "PLxxxxxxxxxxxxx",
"title": "Tutorials",
"privacy": "public",
"itemCount": 12
}
],
"defaultPlaylistId": null
}
```
Then pass the id when creating the post:
```json
{
"platform": "youtube",
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"platformSpecificData": {
"title": "Build a REST API from scratch",
"visibility": "public",
"playlistId": "PLxxxxxxxxxxxxx"
}
}
```
`PUT /v1/accounts/{accountId}/youtube-playlists` ([Set default YouTube playlist](/connect/update-youtube-default-playlist)) stores a default playlist on the account to prefill your own UI. It does not apply to posts that omit `playlistId`:
```typescript
const { data: saved } = await zernio.connect.updateYoutubeDefaultPlaylist({
path: { accountId: '66b2e19d8c3f5a7e9d0b1c2d' },
body: { defaultPlaylistId: 'PLxxxxxxxxxxxxx', defaultPlaylistName: 'Tutorials' }
});
console.log(saved.success);
```
```python
saved = client.connect.update_youtube_default_playlist(
account_id="66b2e19d8c3f5a7e9d0b1c2d",
default_playlist_id="PLxxxxxxxxxxxxx",
default_playlist_name="Tutorials"
)
print(saved["success"])
```
```bash
curl -X PUT https://zernio.com/api/v1/accounts/66b2e19d8c3f5a7e9d0b1c2d/youtube-playlists \
-H "Authorization: Bearer $ZERNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"defaultPlaylistId": "PLxxxxxxxxxxxxx",
"defaultPlaylistName": "Tutorials"
}'
```
Response (`200`):
```json
{ "success": true }
```
### Scheduling
A post with `scheduledFor` in the future runs in this order:
1. Zernio uploads the video ahead of its scheduled time, so YouTube has finished processing it before the video goes live.
2. A video targeting `"public"` goes up as `"private"` and carries YouTube's own `publishAt`, so YouTube releases it at the scheduled second. A video targeting `"private"` or `"unlisted"` is uploaded with that visibility and never changes.
3. A video URL exists as soon as the upload finishes, but the video is not publicly accessible before the release.
4. `firstComment` is posted at the scheduled time, not at upload time.
```json
{
"platform": "youtube",
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"scheduledFor": "2027-01-01T12:00:00",
"platformSpecificData": {
"title": "Build a REST API from scratch",
"visibility": "public",
"firstComment": "Chapters and source code are in the description."
}
}
```
Set `timezone` on the request so the scheduled time is read in the right zone; see [post lifecycle](/guides/post-lifecycle).
### Edit a published video
[Edit post](/posts/edit-post) replaces the video description only. There is no time window and no limit on the number of edits, and the video id does not change, so the video keeps its URL. The title is left exactly as it was, including when it was derived from the first line of `content` at publish time. Title, tags, thumbnail and visibility changes go through [Update post metadata](/posts/update-post-metadata), which also works on videos uploaded outside Zernio when you pass `videoId` and `accountId` with `_` as the post id.
```typescript
const { data: edited } = await zernio.posts.editPost({
path: { postId: '65f1c0a9e2b5af0012ab34cd' },
body: {
platform: 'youtube',
content: 'Updated description with corrected chapter timestamps.'
}
});
console.log(edited.url);
```
```python
edited = client.posts.edit_post(
post_id="65f1c0a9e2b5af0012ab34cd",
platform="youtube",
content="Updated description with corrected chapter timestamps."
)
print(edited["url"])
```
```bash
curl -X POST https://zernio.com/api/v1/posts/65f1c0a9e2b5af0012ab34cd/edit \
-H "Authorization: Bearer $ZERNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"platform": "youtube",
"content": "Updated description with corrected chapter timestamps."
}'
```
Response (`200`):
```json
{
"success": true,
"id": "dQw4w9WgXcQ",
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"message": "youtube post edited successfully"
}
```
The new description is sanitized before it is written: angle brackets (`<` and `>`) are stripped, and anything past 5,000 characters is truncated. Neither one fails the call, so a request that hits either rule still returns success with the video id and URL. If the post was published to several YouTube channels, pass `accountId` to pick which copy to edit; without it, the first `youtube` entry on the post is edited.
## Platform fields
All fields go in `platformSpecificData` on the YouTube entry.
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `title` | string | First line of `content`, or `"Untitled Video"` | Video title, at most 100 characters. |
| `visibility` | `"public"` \| `"private"` \| `"unlisted"` | `"public"` | Who can see the video. |
| `madeForKids` | boolean | `false` | Marks the video as child-directed for COPPA. `true` permanently disables comments, the notification bell, personalized ads, end screens and cards on the video. YouTube may block views when the flag is never set. |
| `containsSyntheticMedia` | boolean | `false` | Discloses that the video contains synthetic content that could be mistaken for real. YouTube may add a label to the video. |
| `categoryId` | string | `"22"` (People & Blogs) | Video category. Common values: `"1"` Film, `"10"` Music, `"20"` Gaming, `"22"` People & Blogs, `"27"` Education, `"28"` Science & Technology. |
| `playlistId` | string | | Playlist to add the video to after upload. See [Playlists](#playlists). |
| `firstComment` | string | | Posted and pinned as the first comment, at most 10,000 characters. Posted immediately with `publishNow`, at the scheduled time otherwise. |
## Media requirements
Files above these limits are rejected: 256 GB per video, 15 minutes on an unverified channel, 2 MB per thumbnail after compression. Large videos (1 GB or more) can take 30 to 60 minutes or longer to process on YouTube's side; the video shows a "processing" state meanwhile, so do not retry the upload.
### Videos
| Property | Shorts | Video |
|----------|--------|-------|
| Max duration | 3 minutes | 12 hours (verified), 15 minutes (unverified) |
| Min duration | 1 second | 1 second |
| Max file size | 256 GB | 256 GB |
| Formats | MP4, MOV, AVI, WMV, FLV, 3GP, WebM | MP4, MOV, AVI, WMV, FLV, 3GP, WebM |
| Aspect ratio | 9:16 (vertical) | 16:9 (horizontal) |
| Resolution | 1080 x 1920 px | 1920 x 1080 px (1080p) |
Recommended encoding:
| Property | Shorts | Video |
|----------|--------|-------|
| Resolution | 1080 x 1920 px | 3840 x 2160 px (4K) |
| Frame rate | 30 fps | 24 to 60 fps |
| Codec | H.264 | H.264 or H.265 |
| Audio | AAC, 128 kbps | AAC, 384 kbps |
| Bitrate | 10 Mbps | 35 to 68 Mbps (4K) |
### Custom thumbnails
Custom thumbnails work on videos only, not Shorts. Set `thumbnail` on the video media item.
| Property | Requirement |
|----------|-------------|
| Format | JPEG, PNG, GIF |
| Max size | 2 MB |
| Recommended resolution | 1280 x 720 px (16:9) |
| Min width | 640 px |
Zernio enforces YouTube's rules before upload: JPEG, PNG or GIF, and 2 MB at most (oversized images are compressed first, and rejected if they are still over 2 MB). YouTube itself only accepts custom thumbnails on phone-verified channels ([youtube.com/verify](https://www.youtube.com/verify)). On an unverified channel the video still uploads and publishes, only the thumbnail is skipped; Zernio remembers the refusal for 7 days and does not retry thumbnails on that channel until then, so after verifying allow up to a week for thumbnails to resume.
### Media URLs
The URL must return the video bytes, not an HTML page, with no authentication and no expired link. Or upload through the [media endpoint](/guides/media-uploads).
## Analytics
Call `GET /v1/analytics?platform=youtube` ([Analytics API](/analytics/get-analytics)).
| Metric | Available |
|--------|-----------|
| Likes | |
| Comments | |
| Shares | (through Daily views only) |
| Views | |
```typescript
const { data: analytics } = await zernio.analytics.getAnalytics({
query: { platform: 'youtube', fromDate: '2026-08-01', toDate: '2026-08-31' }
});
console.log(analytics.posts);
```
```python
analytics = client.analytics.get_analytics(
platform="youtube",
from_date="2026-08-01",
to_date="2026-08-31"
)
print(analytics["posts"])
```
```bash
curl "https://zernio.com/api/v1/analytics?platform=youtube&fromDate=2026-08-01&toDate=2026-08-31" \
-H "Authorization: Bearer $ZERNIO_API_KEY"
```
Response (`200`), one entry per post:
```json
{
"posts": [
{
"_id": "65f1c0a9e2b5af0012ab34cd",
"platform": "youtube",
"status": "published",
"publishedAt": "2026-08-14T10:00:05Z",
"platformPostUrl": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"analytics": {
"likes": 342,
"comments": 28,
"shares": 0,
"views": 15420
}
}
]
}
```
Four YouTube-only endpoints go deeper:
- [Daily views](/analytics/get-youtube-daily-views): per-day views, watch time, subscriber changes and per-day likes, comments and shares for one video. Data has a 2 to 3 day delay.
- [Channel insights](/analytics/get-youtube-channel-insights): channel-level views, watch time, average view duration and subscribers gained and lost, without looping through every video. Impressions and impressions click-through rate (the thumbnail metrics in YouTube Studio) are not exposed by YouTube's Analytics API v2 for any principal type; the only way to get those is a manual Studio CSV export.
- [Demographics](/analytics/get-youtube-demographics): audience by age, gender and country, for the channel or one video. Age and gender values are viewer percentages (0 to 100), country values are view counts. Based on signed-in viewers only, with a 2 to 3 day delay.
- [Video retention](/analytics/get-youtube-video-retention): the audience retention curve of one video, up to 100 points over the whole date range.
## Transcripts and captions
To transcribe a YouTube video, read the caption track YouTube already holds for it rather than downloading the file and running your own transcription. Call `GET /v1/accounts/{accountId}/youtube-captions` with `videoId` ([Get a YouTube video transcript](/connect/get-youtube-captions)), for one of the connected channel's own videos. Auto-generated (ASR) tracks count: YouTube serves them to the channel owner, which is what the connected account is. An uploaded track wins over an auto-generated one for the same language.
```typescript
const { data: transcript } = await zernio.connect.getYoutubeCaptions({
path: { accountId: '66b2e19d8c3f5a7e9d0b1c2d' },
query: { videoId: 'dQw4w9WgXcQ', language: 'en' }
});
console.log(transcript.text);
```
```python
transcript = client.connect.get_youtube_captions(
account_id="66b2e19d8c3f5a7e9d0b1c2d",
video_id="dQw4w9WgXcQ",
language="en"
)
print(transcript["text"])
```
```bash
curl "https://zernio.com/api/v1/accounts/66b2e19d8c3f5a7e9d0b1c2d/youtube-captions?videoId=dQw4w9WgXcQ&language=en" \
-H "Authorization: Bearer $ZERNIO_API_KEY"
```
Response (`200`), trimmed:
```json
{
"videoId": "dQw4w9WgXcQ",
"language": "en",
"trackKind": "asr",
"source": "cache",
"fetchedAt": "2026-08-27T21:09:54.000Z",
"text": "Hey, this is Mickey. I'm the founder of this portfolio of three websites.",
"cues": [
{ "start": 1.6, "end": 8.88, "text": "Hey, this is Mickey. I'm the founder of" }
]
}
```
`format=srt` returns the raw SubRip body in `srt` instead of `cues`; `text` is there either way. The first read downloads from YouTube and costs 200 quota units, and Zernio stores the result, so `source` reads `youtube` once and `cache` afterwards. Pass `refresh=true` only when the captions changed on YouTube, since that spends the quota again. `availableTracks` lists every track on the video so you can request another language.
Only videos owned by the connected channel are readable; anything else is a `404`. A video with no track in the requested language is also a `404`, with `code: "captions_not_found"`. YouTube generates auto-captions only for videos with recognizable speech and can take a few hours after upload to publish them, so treat that `404` as "not yet" rather than "never". `contentDetails.caption` in YouTube's own API reads `false` on videos that do have a serving auto-generated track, so it is not a usable availability signal.
## Inbox
YouTube supports comments only; the platform has no DMs.
| Feature | Supported |
|---------|-----------|
| List comments on videos | |
| Reply to comments | |
| Delete comments | |
| Moderate comments | (YouTube only: approve, reject or hold, with an optional author ban) |
| Like comments | (no API available) |
Read and reply with the [Comments API](/comments/list-inbox-comments). Work a moderation queue with `POST /v1/inbox/comments/{postId}/{commentId}/moderation`: `moderationStatus` takes `published` to approve, `rejected` to remove or `heldForReview` to send it back to the queue, and `banAuthor: true` (valid only alongside `rejected`) auto-rejects that author from then on. You have to own the channel or the video ([Set comment moderation status](/comments/set-comment-moderation)).
Like a video as any connected channel with `POST /v1/inbox/posts/{postId}/like`, and clear the rating again with `DELETE`. Each call spends 50 of the project's 10,000 daily quota units, the tightest per-day ceiling of any platform here, and a video whose owner turned ratings off returns `403`.
## What you cannot do
YouTube's API does not expose:
- Community posts
- Going live or scheduling Premieres
- End screens, cards or chapters (timestamps in the description do work)
- Monetization settings
- Creating or deleting playlists (you can list playlists and add videos to an existing one)
- Disliking a video (YouTube's rating call takes a like or no rating, so a like can only be set or cleared)
- Uploading captions or subtitles (reading an existing track is supported; see [Transcripts and captions](#transcripts-and-captions))
- Liking comments
## Common errors
| Error | Cause | Fix |
|-------|-------|-----|
| "The YouTube account of the authenticated user is suspended." (`403`) | YouTube suspended the channel | Check the channel status on YouTube. Use [Account health](/accounts/get-all-accounts-health). |
| "Social account not found" | The account was disconnected or deleted from Zernio | Reconnect the YouTube account. Subscribe to the `account.disconnected` webhook. |
| "Account was deleted" | The user deleted the account | Reconnect the account. |
| "Failed to fetch video from URL: 404" | The video URL returned a 404 | Check that the URL is still valid and public. Links expire on some hosts. |
| "YouTube permission error: Ensure the channel has required scopes and features enabled." | The OAuth token lacks a required scope | Reconnect the YouTube account and grant every scope. |
| "YouTube upload initialization failed: 403" | YouTube rejected the upload before the file transfer began | Check whether the channel is suspended, the upload quota is exhausted, or permissions are missing. |
A `publishNow: true` post that YouTube rejects returns `207` with `post.status: "failed"` and the message in `platforms[].errorMessage`:
```json
{
"message": "Post created but publishing failed",
"error": "All platforms failed",
"post": {
"_id": "65f1c0a9e2b5af0012ab34cd",
"status": "failed",
"platforms": [
{
"platform": "youtube",
"status": "failed",
"errorMessage": "The YouTube account of the authenticated user is suspended."
}
]
}
}
```
`207` is a 2xx status, so `fetch(...).ok` is `true`; branch on the status code and on `post.status`. [Error handling](/guides/error-handling) covers the envelope.
## Related
- [Connecting accounts](/guides/connecting-accounts): the OAuth flow.
- [Create post](/posts/create-post): every field of the request.
- [Edit post](/posts/edit-post) and [Update post metadata](/posts/update-post-metadata): change a published video.
- [Media uploads](/guides/media-uploads): upload videos instead of hosting them.
- [Analytics](/analytics/get-analytics): post performance metrics.
- [Comments](/comments/list-inbox-comments): read and reply to comments.
- [Get a YouTube video transcript](/connect/get-youtube-captions): every parameter of the captions call.
- [Pricing](/pricing): what analytics, the inbox and outbound messages cost, and which replies count.
---
# Resources
Create posts from n8n, Make, Zapier, Chat SDK or OpenClaw, generate a client from the OpenAPI spec, or move from Ayrshare, Kapso or Twilio.
import { Cards, Card } from 'fumadocs-ui/components/card';
import { Workflow, Code, ArrowRightLeft } from 'lucide-react';
Pick by where the calling code lives. Integrations put Zernio inside something that already runs (n8n, Make, Zapier, OpenClaw, or a bot you write with the Chat SDK), so there is no client to maintain. Open source is for code you own: generate a client from the OpenAPI spec, or fork a project already built on the API. Migrations are for code that already calls another provider, and each guide maps that provider's fields onto Zernio's. Every page here uses the same [API key](https://zernio.com/dashboard/api-keys) and the endpoints in the [API reference](/).
}
title="Integrations"
description="Create posts from n8n, Make, Zapier and OpenClaw, or build a chatbot with the Chat SDK adapter."
href="/resources/integrations"
/>
}
title="Open source"
description="SDKs, the OpenAPI specification and the projects built on the Zernio API."
href="/resources/open-source"
/>
## Migrations
Each guide also gives a cutover order that does not double-post.
}
title="From Ayrshare"
description="Social posting, profiles and analytics."
href="/resources/migrations/migrating-from-ayrshare"
/>
}
title="From Kapso"
description="WhatsApp messaging."
href="/resources/migrations/migrating-from-kapso"
/>
}
title="From Twilio"
description="WhatsApp, SMS and voice."
href="/resources/migrations/migrating-from-twilio"
/>
---
# Open Source
Generate a client from the Zernio OpenAPI spec, use one of the platform specs, or start from an open-source project built on the API.
import { Cards, Card } from 'fumadocs-ui/components/card';
import { Store } from 'lucide-react';
import { SiBluesky, SiFacebook, SiInstagram, SiLinkedin, SiPinterest, SiReddit, SiSnapchat, SiTelegram, SiThreads, SiTiktok, SiWhatsapp, SiX, SiYoutube } from 'react-icons/si';
Use the Zernio OpenAPI spec to generate a client, take one of the platform specs, or start from an open-source project built on the API. The Zernio spec is at [zernio.com/openapi.yaml](https://zernio.com/openapi.yaml); the official clients built from it are on the [SDKs page](/sdks).
export const GithubIcon = () => (
);
## Generate a Zernio client
Point a generator at [zernio.com/openapi.yaml](https://zernio.com/openapi.yaml):
```bash
openapi-generator-cli generate \
-i https://zernio.com/openapi.yaml \
-g typescript-fetch -o ./zernio-sdk
```
A generated client that returns `401` on every call is sending no credentials. The spec's global requirement is HTTP bearer, so set the API key on the generated configuration object before the first call. The generator also picks up a second scheme, `connectToken` (the `X-Connect-Token` header), which the page and location selection operations under `/v1/connect` accept as an alternative, so the configuration carries two auth settings; leave that one empty unless you are finishing an OAuth flow with no browser session ([connecting accounts](/guides/connecting-accounts)).
## Platform API specs
OpenAPI specifications for the platform APIs Zernio publishes to, maintained in the [openapi-specs repository](https://github.com/zernio-dev/openapi-specs). Use them to generate a client, build a request in Postman or read an API in Swagger UI.
}
title="X API"
description="OpenAPI spec for the X API v2: posts, users, spaces and lists"
href="https://github.com/zernio-dev/openapi-specs/blob/main/twitter.yaml"
/>
}
title="Instagram Graph API"
description="OpenAPI spec for the Instagram Graph API: media, comments, insights and stories"
href="https://github.com/zernio-dev/openapi-specs/blob/main/instagram.yaml"
/>
}
title="Facebook Graph API"
description="OpenAPI spec for the Facebook Graph API: pages, posts, comments and ads"
href="https://github.com/zernio-dev/openapi-specs/blob/main/facebook.yaml"
/>
}
title="LinkedIn API"
description="OpenAPI spec for the LinkedIn Marketing and Community APIs: posts, organizations and analytics"
href="https://github.com/zernio-dev/openapi-specs/blob/main/linkedin.yaml"
/>
}
title="TikTok API"
description="OpenAPI spec for the TikTok Business APIs: video and photo publishing, creator info, comments and analytics"
href="https://github.com/zernio-dev/openapi-specs/blob/main/tiktok.yaml"
/>
}
title="YouTube Data API"
description="OpenAPI spec for the YouTube Data API v3: videos, channels, playlists and comments"
href="https://github.com/zernio-dev/openapi-specs/blob/main/youtube.yaml"
/>
}
title="Pinterest API"
description="OpenAPI spec for the Pinterest API v5: pins, boards and analytics"
href="https://github.com/zernio-dev/openapi-specs/blob/main/pinterest.yaml"
/>
}
title="Reddit API"
description="OpenAPI spec for the Reddit API: posts, comments, subreddits and users"
href="https://github.com/zernio-dev/openapi-specs/blob/main/reddit.yaml"
/>
}
title="Threads API"
description="OpenAPI spec for the Threads API: posts, replies and user profiles"
href="https://github.com/zernio-dev/openapi-specs/blob/main/threads.yaml"
/>
}
title="Bluesky API (AT Protocol)"
description="OpenAPI spec for the Bluesky AT Protocol: posts, follows and feeds"
href="https://github.com/zernio-dev/openapi-specs/blob/main/bluesky.yaml"
/>
}
title="Google Business Profile API"
description="OpenAPI spec for the Google Business Profile API: reviews, posts and locations"
href="https://github.com/zernio-dev/openapi-specs/blob/main/googlebusiness.yaml"
/>
}
title="Telegram Bot API"
description="OpenAPI spec for the Telegram Bot API: messages, updates and inline queries"
href="https://github.com/zernio-dev/openapi-specs/blob/main/telegram.yaml"
/>
}
title="WhatsApp Business API"
description="OpenAPI spec for the WhatsApp Business Platform: messages, templates, media and flows"
href="https://github.com/zernio-dev/openapi-specs/blob/main/whatsapp.yaml"
/>
}
title="Snapchat Marketing API"
description="OpenAPI spec for the Snapchat Marketing API: ads, campaigns and analytics"
href="https://github.com/zernio-dev/openapi-specs/blob/main/snapchat.yaml"
/>
### Use a spec
Each spec has a raw URL: `https://raw.githubusercontent.com/zernio-dev/openapi-specs/main/.yaml`, where `` is the file the card links (`twitter.yaml` for X). Everything below takes that URL.
Import into Postman: copy the raw URL, open Postman, choose File and then Import, and paste it.
Generate a client. The generator reads the raw URL, so there is nothing to download first:
```bash
openapi-generator-cli generate \
-i https://raw.githubusercontent.com/zernio-dev/openapi-specs/main/twitter.yaml \
-g typescript-fetch -o ./twitter-sdk
```
View in Swagger UI:
```bash
docker run -p 8080:8080 -e SWAGGER_JSON_URL=https://raw.githubusercontent.com/zernio-dev/openapi-specs/main/twitter.yaml swaggerapi/swagger-ui
```
Issues and pull requests for the specs go to the [openapi-specs repository](https://github.com/zernio-dev/openapi-specs).
The platform specs are snapshots of other companies' APIs, so open an issue when one drifts from the live platform.
## Projects
Four open-source apps built on the Zernio API, each one a working starting point rather than a sample:
}
title="Latewiz"
description="Open-source social media scheduler built on the Zernio API"
href="https://github.com/zernio-dev/latewiz"
/>
}
title="Zernflow"
description="Open-source visual chatbot builder for Instagram, Facebook, Telegram, X, Bluesky and Reddit"
href="https://github.com/zernio-dev/zernflow"
/>
}
title="Unified Inbox"
description="Open-source inbox for WhatsApp, Instagram, Messenger, Telegram, X, Reddit and Bluesky, built on the Zernio API"
href="https://github.com/zernio-dev/unified-inbox"
/>
}
title="Ads Dashboard"
description="Open-source ads reporting dashboard for the Zernio API. Paste your API key, see your ads data."
href="https://github.com/zernio-dev/ads-dashboard"
/>
---
# Create group API Reference
Creates a new account group with a name and a list of account IDs.
Accounts can belong to different profiles; the caller must have access to
every account's profile. Group names must be unique per user.
## GET /v1/account-groups
**List groups**
Returns all account groups visible to the authenticated user. Groups can
contain accounts from multiple profiles. For API keys scoped to specific
profiles, only groups whose accounts all live in allowed profiles are
returned.
### Responses
#### 200: Groups
**Response Body:**
- **groups** `array[object]`:
- **_id** `string`: No description
- **name** `string`: No description
- **accountIds** `array[string]`:
- **createdBy** `string`: No description
- **profileId** `string`: Legacy field. Present only on groups created before
cross-profile groups were supported. New groups omit it.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
---
## POST /v1/account-groups
**Create group**
Creates a new account group with a name and a list of account IDs.
Accounts can belong to different profiles; the caller must have access to
every account's profile. Group names must be unique per user.
### Request Body
- **name** (required) `string`: No description
- **accountIds** (required) `array`: No description
- **profileId** `string`: Deprecated. Accepted for backward compatibility but ignored.
Groups are no longer scoped to a single profile.
### Responses
#### 201: Created
**Response Body:**
- **message** `string`: No description
- **group** `object`:
- **_id** `string`: No description
- **name** `string`: No description
- **accountIds** `array[string]`:
#### 400: Invalid request
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 409: Group name already exists
---
---
# Delete group API Reference
Permanently deletes an account group. The accounts themselves are not affected.
## PUT /v1/account-groups/{groupId}
**Update group**
Updates the name or account list of an existing group. You can rename the group, change its accounts, or both.
### Parameters
- **groupId** (required) in path: No description
### Request Body
- **name** `string`: No description
- **accountIds** `array`: No description
### Responses
#### 200: Updated
**Response Body:**
- **message** `string`: No description
- **group** `object`: No description
#### 400: Invalid request
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: Resource not found
**Response Body:**
- **error** `string`: No description (example: "Not found")
#### 409: Group name already exists
---
## DELETE /v1/account-groups/{groupId}
**Delete group**
Permanently deletes an account group. The accounts themselves are not affected.
### Parameters
- **groupId** (required) in path: No description
### Responses
#### 200: Deleted
**Response Body:**
- **message** `string`: No description
#### 400: Invalid request
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: Resource not found
**Response Body:**
- **error** `string`: No description (example: "Not found")
---
---
# List groups API Reference
Returns all account groups visible to the authenticated user. Groups can
contain accounts from multiple profiles. For API keys scoped to specific
profiles, only groups whose accounts all live in allowed profiles are
returned.
## GET /v1/account-groups
**List groups**
Returns all account groups visible to the authenticated user. Groups can
contain accounts from multiple profiles. For API keys scoped to specific
profiles, only groups whose accounts all live in allowed profiles are
returned.
### Responses
#### 200: Groups
**Response Body:**
- **groups** `array[object]`:
- **_id** `string`: No description
- **name** `string`: No description
- **accountIds** `array[string]`:
- **createdBy** `string`: No description
- **profileId** `string`: Legacy field. Present only on groups created before
cross-profile groups were supported. New groups omit it.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
---
## POST /v1/account-groups
**Create group**
Creates a new account group with a name and a list of account IDs.
Accounts can belong to different profiles; the caller must have access to
every account's profile. Group names must be unique per user.
### Request Body
- **name** (required) `string`: No description
- **accountIds** (required) `array`: No description
- **profileId** `string`: Deprecated. Accepted for backward compatibility but ignored.
Groups are no longer scoped to a single profile.
### Responses
#### 201: Created
**Response Body:**
- **message** `string`: No description
- **group** `object`:
- **_id** `string`: No description
- **name** `string`: No description
- **accountIds** `array[string]`:
#### 400: Invalid request
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 409: Group name already exists
---
---
# Update group API Reference
Updates the name or account list of an existing group. You can rename the group, change its accounts, or both.
## PUT /v1/account-groups/{groupId}
**Update group**
Updates the name or account list of an existing group. You can rename the group, change its accounts, or both.
### Parameters
- **groupId** (required) in path: No description
### Request Body
- **name** `string`: No description
- **accountIds** `array`: No description
### Responses
#### 200: Updated
**Response Body:**
- **message** `string`: No description
- **group** `object`: No description
#### 400: Invalid request
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: Resource not found
**Response Body:**
- **error** `string`: No description (example: "Not found")
#### 409: Group name already exists
---
## DELETE /v1/account-groups/{groupId}
**Delete group**
Permanently deletes an account group. The accounts themselves are not affected.
### Parameters
- **groupId** (required) in path: No description
### Responses
#### 200: Deleted
**Response Body:**
- **message** `string`: No description
#### 400: Invalid request
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: Resource not found
**Response Body:**
- **error** `string`: No description (example: "Not found")
---
---
# Delete IG ice breakers API Reference
Removes the ice breaker questions from an Instagram account's Messenger experience.
## GET /v1/accounts/{accountId}/instagram-ice-breakers
**Get IG ice breakers**
Get the ice breaker configuration for an Instagram account.
### Parameters
- **accountId** (required) in path: No description
### Responses
#### 200: Ice breaker configuration
**Response Body:**
- **data** `array[object]`:
Type: `object`
#### 400: Not an Instagram account
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
---
## PUT /v1/accounts/{accountId}/instagram-ice-breakers
**Set IG ice breakers**
Set ice breakers for an Instagram account. Max 4 ice breakers, question max 80 chars.
### Parameters
- **accountId** (required) in path: No description
### Request Body
- **ice_breakers** (required) `array`: No description
### Responses
#### 200: Ice breakers set successfully
#### 400: Invalid request
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
---
## DELETE /v1/accounts/{accountId}/instagram-ice-breakers
**Delete IG ice breakers**
Removes the ice breaker questions from an Instagram account's Messenger experience.
### Parameters
- **accountId** (required) in path: No description
### Responses
#### 200: Ice breakers deleted
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Delete FB persistent menu API Reference
Removes the persistent menu from Facebook Messenger conversations for this account.
## GET /v1/accounts/{accountId}/messenger-menu
**Get FB persistent menu**
Get the persistent menu configuration for a Facebook Messenger account.
### Parameters
- **accountId** (required) in path: No description
### Responses
#### 200: Persistent menu configuration
**Response Body:**
- **data** `array[object]`:
Type: `object`
#### 400: Not a Facebook account
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
---
## PUT /v1/accounts/{accountId}/messenger-menu
**Set FB persistent menu**
Set the persistent menu for a Facebook Messenger account. Max 3 top-level items, max 5 nested items.
### Parameters
- **accountId** (required) in path: No description
### Request Body
- **persistent_menu** (required) `array`: Persistent menu configuration array (Meta format)
### Responses
#### 200: Menu set successfully
#### 400: Invalid request
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
---
## DELETE /v1/accounts/{accountId}/messenger-menu
**Delete FB persistent menu**
Removes the persistent menu from Facebook Messenger conversations for this account.
### Parameters
- **accountId** (required) in path: No description
### Responses
#### 200: Menu deleted
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Delete TG bot commands API Reference
Clears all bot commands configured for a Telegram bot account.
## GET /v1/accounts/{accountId}/telegram-commands
**Get TG bot commands**
Get the bot commands configuration for a Telegram account.
### Parameters
- **accountId** (required) in path: No description
### Responses
#### 200: Bot commands list
**Response Body:**
- **data** `array[object]`:
- **command** `string`: No description
- **description** `string`: No description
#### 400: Not a Telegram account
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
---
## PUT /v1/accounts/{accountId}/telegram-commands
**Set TG bot commands**
Set bot commands for a Telegram account.
### Parameters
- **accountId** (required) in path: No description
### Request Body
- **commands** (required) `array`: No description
### Responses
#### 200: Commands set successfully
#### 400: Invalid request
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
---
## DELETE /v1/accounts/{accountId}/telegram-commands
**Delete TG bot commands**
Clears all bot commands configured for a Telegram bot account.
### Parameters
- **accountId** (required) in path: No description
### Responses
#### 200: Commands deleted
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Get IG ice breakers API Reference
Get the ice breaker configuration for an Instagram account.
## GET /v1/accounts/{accountId}/instagram-ice-breakers
**Get IG ice breakers**
Get the ice breaker configuration for an Instagram account.
### Parameters
- **accountId** (required) in path: No description
### Responses
#### 200: Ice breaker configuration
**Response Body:**
- **data** `array[object]`:
Type: `object`
#### 400: Not an Instagram account
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
---
## PUT /v1/accounts/{accountId}/instagram-ice-breakers
**Set IG ice breakers**
Set ice breakers for an Instagram account. Max 4 ice breakers, question max 80 chars.
### Parameters
- **accountId** (required) in path: No description
### Request Body
- **ice_breakers** (required) `array`: No description
### Responses
#### 200: Ice breakers set successfully
#### 400: Invalid request
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
---
## DELETE /v1/accounts/{accountId}/instagram-ice-breakers
**Delete IG ice breakers**
Removes the ice breaker questions from an Instagram account's Messenger experience.
### Parameters
- **accountId** (required) in path: No description
### Responses
#### 200: Ice breakers deleted
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Get FB persistent menu API Reference
Get the persistent menu configuration for a Facebook Messenger account.
## GET /v1/accounts/{accountId}/messenger-menu
**Get FB persistent menu**
Get the persistent menu configuration for a Facebook Messenger account.
### Parameters
- **accountId** (required) in path: No description
### Responses
#### 200: Persistent menu configuration
**Response Body:**
- **data** `array[object]`:
Type: `object`
#### 400: Not a Facebook account
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
---
## PUT /v1/accounts/{accountId}/messenger-menu
**Set FB persistent menu**
Set the persistent menu for a Facebook Messenger account. Max 3 top-level items, max 5 nested items.
### Parameters
- **accountId** (required) in path: No description
### Request Body
- **persistent_menu** (required) `array`: Persistent menu configuration array (Meta format)
### Responses
#### 200: Menu set successfully
#### 400: Invalid request
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
---
## DELETE /v1/accounts/{accountId}/messenger-menu
**Delete FB persistent menu**
Removes the persistent menu from Facebook Messenger conversations for this account.
### Parameters
- **accountId** (required) in path: No description
### Responses
#### 200: Menu deleted
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Get TG bot commands API Reference
Get the bot commands configuration for a Telegram account.
## GET /v1/accounts/{accountId}/telegram-commands
**Get TG bot commands**
Get the bot commands configuration for a Telegram account.
### Parameters
- **accountId** (required) in path: No description
### Responses
#### 200: Bot commands list
**Response Body:**
- **data** `array[object]`:
- **command** `string`: No description
- **description** `string`: No description
#### 400: Not a Telegram account
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
---
## PUT /v1/accounts/{accountId}/telegram-commands
**Set TG bot commands**
Set bot commands for a Telegram account.
### Parameters
- **accountId** (required) in path: No description
### Request Body
- **commands** (required) `array`: No description
### Responses
#### 200: Commands set successfully
#### 400: Invalid request
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
---
## DELETE /v1/accounts/{accountId}/telegram-commands
**Delete TG bot commands**
Clears all bot commands configured for a Telegram bot account.
### Parameters
- **accountId** (required) in path: No description
### Responses
#### 200: Commands deleted
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Set IG ice breakers API Reference
Set ice breakers for an Instagram account. Max 4 ice breakers, question max 80 chars.
## GET /v1/accounts/{accountId}/instagram-ice-breakers
**Get IG ice breakers**
Get the ice breaker configuration for an Instagram account.
### Parameters
- **accountId** (required) in path: No description
### Responses
#### 200: Ice breaker configuration
**Response Body:**
- **data** `array[object]`:
Type: `object`
#### 400: Not an Instagram account
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
---
## PUT /v1/accounts/{accountId}/instagram-ice-breakers
**Set IG ice breakers**
Set ice breakers for an Instagram account. Max 4 ice breakers, question max 80 chars.
### Parameters
- **accountId** (required) in path: No description
### Request Body
- **ice_breakers** (required) `array`: No description
### Responses
#### 200: Ice breakers set successfully
#### 400: Invalid request
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
---
## DELETE /v1/accounts/{accountId}/instagram-ice-breakers
**Delete IG ice breakers**
Removes the ice breaker questions from an Instagram account's Messenger experience.
### Parameters
- **accountId** (required) in path: No description
### Responses
#### 200: Ice breakers deleted
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Set FB persistent menu API Reference
Set the persistent menu for a Facebook Messenger account. Max 3 top-level items, max 5 nested items.
## GET /v1/accounts/{accountId}/messenger-menu
**Get FB persistent menu**
Get the persistent menu configuration for a Facebook Messenger account.
### Parameters
- **accountId** (required) in path: No description
### Responses
#### 200: Persistent menu configuration
**Response Body:**
- **data** `array[object]`:
Type: `object`
#### 400: Not a Facebook account
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
---
## PUT /v1/accounts/{accountId}/messenger-menu
**Set FB persistent menu**
Set the persistent menu for a Facebook Messenger account. Max 3 top-level items, max 5 nested items.
### Parameters
- **accountId** (required) in path: No description
### Request Body
- **persistent_menu** (required) `array`: Persistent menu configuration array (Meta format)
### Responses
#### 200: Menu set successfully
#### 400: Invalid request
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
---
## DELETE /v1/accounts/{accountId}/messenger-menu
**Delete FB persistent menu**
Removes the persistent menu from Facebook Messenger conversations for this account.
### Parameters
- **accountId** (required) in path: No description
### Responses
#### 200: Menu deleted
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Set TG bot commands API Reference
Set bot commands for a Telegram account.
## GET /v1/accounts/{accountId}/telegram-commands
**Get TG bot commands**
Get the bot commands configuration for a Telegram account.
### Parameters
- **accountId** (required) in path: No description
### Responses
#### 200: Bot commands list
**Response Body:**
- **data** `array[object]`:
- **command** `string`: No description
- **description** `string`: No description
#### 400: Not a Telegram account
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
---
## PUT /v1/accounts/{accountId}/telegram-commands
**Set TG bot commands**
Set bot commands for a Telegram account.
### Parameters
- **accountId** (required) in path: No description
### Request Body
- **commands** (required) `array`: No description
### Responses
#### 200: Commands set successfully
#### 400: Invalid request
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
---
## DELETE /v1/accounts/{accountId}/telegram-commands
**Delete TG bot commands**
Clears all bot commands configured for a Telegram bot account.
### Parameters
- **accountId** (required) in path: No description
### Responses
#### 200: Commands deleted
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Disconnect account API Reference
Disconnects and removes a connected account. Repeating the call for an account already disconnected returns 404, the account stays in its 1h grace window and the disconnect is not re-run.
## PUT /v1/accounts/{accountId}
**Update account**
Updates a connected account's display name or username override.
For X accounts on usage-based billing, also accepts an `xCapabilities`
object to toggle background API operations that incur X API pass-through costs.
Both fields are opt-in (default `false`). When off, no analytics syncs or DM
polling are performed for that account, and no API call is metered for those
operations. Publishing and deleting posts are always available regardless of
these toggles. Setting `xCapabilities` on a non-X account returns 400.
### Parameters
- **accountId** (required) in path: No description
### Request Body
- **username** `string`: No description
- **displayName** `string`: No description
- **xCapabilities** `object`: X only. Per-account opt-in toggles for background API
operations that incur X API pass-through costs. Each call is
billed at the X tier rate. Either field can be
sent independently; omitted fields are unchanged.
### Responses
#### 200: Updated
**Response Body:**
- **message** `string`: No description
- **username** `string`: No description
- **displayName** `string`: No description
- **xCapabilities** `object`: Echo of the resulting `xCapabilities` state, returned only
when the request body included an `xCapabilities` object.
- **analytics** `boolean`: No description
- **inbox** `boolean`: No description
#### 400: Invalid request (e.g. xCapabilities on a non-X account)
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: Resource not found
**Response Body:**
- **error** `string`: No description (example: "Not found")
---
## PATCH /v1/accounts/{accountId}
**Move account to another profile**
Moves a connected account to a different profile owned by the same
user. The target profile must belong to the same user as the account.
For API keys restricted to specific profiles, BOTH the source account's
current profile AND the target profile must be in the key's allowed set.
Calls with a target profile outside the key's scope return 403.
### Parameters
- **accountId** (required) in path: No description
### Request Body
- **profileId** (required) `string`: Target profile ID (must be a valid ObjectId and owned by the same user as the account).
### Responses
#### 200: Account moved
**Response Body:**
- **message** `string`: No description
- **profileId** `string`: No description
#### 400: Missing or invalid profileId
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: API key does not have access to the source account or target profile
#### 404: Account or target profile not found
---
## DELETE /v1/accounts/{accountId}
**Disconnect account**
Disconnects and removes a connected account. Repeating the call for an account already disconnected returns 404, the account stays in its 1h grace window and the disconnect is not re-run.
### Parameters
- **accountId** (required) in path: No description
### Responses
#### 200: Disconnected
**Response Body:**
- **message** `string`: No description
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: Resource not found
**Response Body:**
- **error** `string`: No description (example: "Not found")
---
---
# Check account health API Reference
Returns detailed health info for a specific account including token status, permissions, and recommendations.
For WhatsApp accounts the response also includes `platformConnection`, a live probe of the
Meta link behind the channel (the same read as `GET /v1/whatsapp/number-info`). The OAuth
token can be perfectly valid while Meta refuses to serve the phone-number object (for
example after a phone-side coexistence disconnect), so `tokenStatus` alone is not a
liveness signal for WhatsApp. When the Meta link is dead, `platformConnection.status` is
`disconnected` and the overall `status` is `error`.
## GET /v1/accounts/{accountId}/health
**Check account health**
Returns detailed health info for a specific account including token status, permissions, and recommendations.
For WhatsApp accounts the response also includes `platformConnection`, a live probe of the
Meta link behind the channel (the same read as `GET /v1/whatsapp/number-info`). The OAuth
token can be perfectly valid while Meta refuses to serve the phone-number object (for
example after a phone-side coexistence disconnect), so `tokenStatus` alone is not a
liveness signal for WhatsApp. When the Meta link is dead, `platformConnection.status` is
`disconnected` and the overall `status` is `error`.
### Parameters
- **accountId** (required) in path: The account ID to check
### Responses
#### 200: Account health details
**Response Body:**
- **accountId** `string`: No description
- **platform** `string`: No description
- **username** `string`: No description
- **displayName** `string`: No description
- **status** `string`: Overall health status - one of: healthy, warning, error
- **tokenStatus** `object`:
- **valid** `boolean`: Whether the token is valid
- **expiresAt** `string` (date-time): No description
- **expiresIn** `string`: Human-readable time until expiry
- **needsRefresh** `boolean`: Whether token expires within 24 hours
- **permissions** `object`:
- **posting** `array[object]`:
- **scope** `string`: No description
- **granted** `boolean`: No description
- **required** `boolean`: No description
- **analytics** `array[object]`:
- **scope** `string`: No description
- **granted** `boolean`: No description
- **required** `boolean`: No description
- **optional** `array[object]`:
- **scope** `string`: No description
- **granted** `boolean`: No description
- **required** `boolean`: No description
- **canPost** `boolean`: No description
- **canFetchAnalytics** `boolean`: No description
- **missingRequired** `array[string]`:
- **issues** `array[string]`: List of issues found
- **recommendations** `array[string]`: Actionable recommendations to fix issues
- **messagingRestriction** `object,null`: Observed from Meta's own error subcodes on our own sends (2534122, 1893063, 2534029), not a live probe. Set on the first refused send and cleared when a later send succeeds, so it lags reality by one send in each direction.
- **platformConnection** `object`: WhatsApp accounts only. Live probe of the Meta link behind the channel, performed at request time (the same read as GET /v1/whatsapp/number-info).
- **status** `string`: `connected` = Meta served the channel object. `disconnected` = Meta refused to serve it (Graph error 100, subcode 33), which is how a phone-side coexistence disconnect surfaces. `unknown` = the live read failed for another reason (timeout, transient Meta error), not evidence either way. - one of: connected, disconnected, unknown
- **checkedAt** `string` (date-time): When this live probe ran (always the current request; never cached)
- **phoneStatus** `string,null`: Meta's own `status` field from the phone-number node (for example CONNECTED), when the object was readable
- **metaError** `object,null`: Set only when status is `disconnected`
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: Resource not found
**Response Body:**
- **error** `string`: No description (example: "Not found")
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# List posts published on the platform API Reference
Returns the 25 most recent posts that exist on the platform for a connected account, read
live from the platform API. This covers everything on the account, including posts that
were never created through Zernio.
Use it to obtain the platform's own post id, which the analytics endpoints take as input.
On YouTube the returned `id` is the video ID that `GET /v1/analytics/youtube/daily-views`,
`/video-retention` and `/demographics` expect as `videoId`, so this endpoint is what backs
a video picker in your own UI.
Not every field applies to every platform: `reactionCount` is Facebook and LinkedIn,
`shareCount` is platform dependent, `cid` is the Bluesky content id needed to reply, and
`subreddit` is Reddit only. Absent fields are omitted from the response.
The account's token is refreshed before the call when it has expired. When the refresh
cannot recover it, the response is a 401 with code `TOKEN_EXPIRED` and the account has to
be reconnected.
## GET /v1/accounts/{accountId}/posts
**List posts published on the platform**
Returns the 25 most recent posts that exist on the platform for a connected account, read
live from the platform API. This covers everything on the account, including posts that
were never created through Zernio.
Use it to obtain the platform's own post id, which the analytics endpoints take as input.
On YouTube the returned `id` is the video ID that `GET /v1/analytics/youtube/daily-views`,
`/video-retention` and `/demographics` expect as `videoId`, so this endpoint is what backs
a video picker in your own UI.
Not every field applies to every platform: `reactionCount` is Facebook and LinkedIn,
`shareCount` is platform dependent, `cid` is the Bluesky content id needed to reply, and
`subreddit` is Reddit only. Absent fields are omitted from the response.
The account's token is refreshed before the call when it has expired. When the refresh
cannot recover it, the response is a 401 with code `TOKEN_EXPIRED` and the account has to
be reconnected.
### Parameters
- **accountId** (required) in path: No description
### Responses
#### 200: Posts list
**Response Body:**
- **status** `string`: No description - one of: success
- **posts** `array[object]`:
- **id** `string`: The platform's own post id (the video ID on YouTube)
- **platform** `string`: No description
- **message** `string`: Caption or title, empty string when the post has no text
- **createdTime** `string` (date-time): No description
- **permalink** `string`: Public URL of the post on the platform
- **picture** `string`: Thumbnail or media URL
- **mediaType** `string`: No description
- **commentCount** `integer`: No description
- **likeCount** `integer`: No description
- **reactionCount** `integer`: Facebook and LinkedIn only
- **shareCount** `integer`: No description
- **cid** `string`: Bluesky content id, required to reply to the post
- **subreddit** `string`: Reddit only
- **lastUpdated** `string` (date-time): No description
#### 400: Invalid accountId, platform does not support posts listing, or the account has no access token
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: X analytics capability not enabled for this account (code X_ANALYTICS_NOT_ENABLED)
#### 404: Resource not found
**Response Body:**
- **error** `string`: No description (example: "Not found")
#### 502: The platform returned a server error.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 503: An upstream service or database is temporarily unavailable. Retry after the indicated delay. A timed-out write may have completed upstream; check its outcome before resubmitting.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Check accounts health API Reference
Returns health status of all connected accounts including token validity, permissions, and issues needing attention.
## GET /v1/accounts/health
**Check accounts health**
Returns health status of all connected accounts including token validity, permissions, and issues needing attention.
### Parameters
- **profileId** (optional) in query: Filter by profile ID
- **platform** (optional) in query: Filter by platform
- **status** (optional) in query: Filter by health status
### Responses
#### 200: Account health summary
**Response Body:**
- **summary** `object`:
- **total** `integer`: Total number of accounts
- **healthy** `integer`: Number of healthy accounts
- **warning** `integer`: Number of accounts with warnings
- **error** `integer`: Number of accounts with errors
- **needsReconnect** `integer`: Number of accounts needing reconnection
- **accounts** `array[object]`:
- **accountId** `string`: No description
- **platform** `string`: No description
- **username** `string`: No description
- **displayName** `string`: No description
- **profileId** `string`: No description
- **status** `string`: No description - one of: healthy, warning, error
- **canPost** `boolean`: No description
- **canFetchAnalytics** `boolean`: No description
- **tokenValid** `boolean`: No description
- **tokenExpiresAt** `string` (date-time): No description
- **needsReconnect** `boolean`: No description
- **issues** `array[string]`:
- **messagingRestriction** `object,null`: Observed from Meta's own error subcodes on our own sends (2534122, 1893063, 2534029), not a live probe. Set on the first refused send and cleared when a later send succeeds, so it lags reality by one send in each direction.
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Get Bluesky account settings API Reference
Returns the account's default post languages (defaultLangs), applied at publish time whenever a post's platformSpecificData.langs is absent. Null when no default is set.
## GET /v1/accounts/{accountId}/bluesky-settings
**Get Bluesky account settings**
Returns the account's default post languages (defaultLangs), applied at publish time whenever a post's platformSpecificData.langs is absent. Null when no default is set.
### Parameters
- **accountId** (required) in path: No description
### Responses
#### 200: Bluesky account settings
**Response Body:**
- **defaultLangs** `array,null`: No description (example: ["pt"])
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: Account not found
---
## PATCH /v1/accounts/{accountId}/bluesky-settings
**Update Bluesky account settings**
Set or clear the account's default post languages. 1-3 BCP-47 codes (e.g. "pt", "en-US"), the same validation as per-post langs; explicit null clears the default. Per-post platformSpecificData.langs always overrides this default. Applies to posts published after the change; already-published posts cannot be retagged (Bluesky has no post edit).
### Parameters
- **accountId** (required) in path: No description
### Request Body
- **defaultLangs** (required) `array,null`: No description
### Responses
#### 200: Updated settings
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: Account not found
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Get follower stats API Reference
Returns follower count history and growth metrics for connected accounts.
Requires analytics add-on subscription. Follower counts are refreshed once per day.
## GET /v1/accounts/follower-stats
**Get follower stats**
Returns follower count history and growth metrics for connected accounts.
Requires analytics add-on subscription. Follower counts are refreshed once per day.
### Parameters
- **accountIds** (optional) in query: Comma-separated list of account IDs (optional, defaults to all user's accounts)
- **profileId** (optional) in query: Filter by profile ID
- **fromDate** (optional) in query: Start date in YYYY-MM-DD format (defaults to 30 days ago)
- **toDate** (optional) in query: End date in YYYY-MM-DD format (defaults to today)
- **granularity** (optional) in query: Data aggregation level
### Responses
#### 200: Follower stats
**Response Body:**
- **accounts** `array[AccountWithFollowerStats]`:
- **stats** `object`: No description
- **dateRange** `object`:
- **from** `string` (date-time): No description
- **to** `string` (date-time): No description
- **granularity** `string`: No description
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Analytics access required. Legacy plans need the Analytics add-on; included by default on usage-based plans.
**Response Body:**
- **error** `string`: No description (example: "Analytics add-on required")
- **message** `string`: No description (example: "Follower stats tracking requires the Analytics add-on. Please upgrade to access this feature.")
- **requiresAddon** `boolean`: No description (example: true)
---
# Related Schema Definitions
## FollowerStatsResponse
### Properties
- **accounts** `array`: No description
- **stats** `object`: No description
- **dateRange** `object`:
- **from** `string`:
- **to** `string`:
- **granularity** `string`: No description
## AccountWithFollowerStats
---
# Check whether an Instagram user follows the account API Reference
Resolves the follow relationship between an Instagram user and the connected
account, plus their public profile counters.
`userId` is the Instagram-scoped id (IGSID) Meta gives you on a webhook:
`sender.id` on `message.received`, `comment.author.id` on `comment.received`.
**Meta only answers for people who have MESSAGED the account.** Commenting grants
no consent, so a commenter who has never DMed you is unresolvable - that is a
platform rule, not a limitation of this endpoint. When it cannot be resolved the
response is still `200` with `isFollower: null` and an `unavailableReason`, because
"unknown" is a normal state to branch on:
* `consent_required` - the user has never messaged this account.
* `dm_access_disabled` - the account owner turned off Instagram Direct API access.
* `not_messageable` - the id is not a messaging-scoped id.
* `error` - a transient Graph API failure.
To gate a comment automation on this, use the automation's `audience` rules instead
of calling this per comment - they run the same lookup only on comments that
actually match a keyword, and can ask the commenter to confirm with one tap.
Answers are cached briefly per (account, user). Pass `refresh=true` right after
asking someone to follow, so a follow from a moment ago is visible.
## GET /v1/accounts/{accountId}/follow-status/{userId}
**Check whether an Instagram user follows the account**
Resolves the follow relationship between an Instagram user and the connected
account, plus their public profile counters.
`userId` is the Instagram-scoped id (IGSID) Meta gives you on a webhook:
`sender.id` on `message.received`, `comment.author.id` on `comment.received`.
**Meta only answers for people who have MESSAGED the account.** Commenting grants
no consent, so a commenter who has never DMed you is unresolvable - that is a
platform rule, not a limitation of this endpoint. When it cannot be resolved the
response is still `200` with `isFollower: null` and an `unavailableReason`, because
"unknown" is a normal state to branch on:
* `consent_required` - the user has never messaged this account.
* `dm_access_disabled` - the account owner turned off Instagram Direct API access.
* `not_messageable` - the id is not a messaging-scoped id.
* `error` - a transient Graph API failure.
To gate a comment automation on this, use the automation's `audience` rules instead
of calling this per comment - they run the same lookup only on comments that
actually match a keyword, and can ask the commenter to confirm with one tap.
Answers are cached briefly per (account, user). Pass `refresh=true` right after
asking someone to follow, so a follow from a moment ago is visible.
### Parameters
- **accountId** (required) in path: Instagram account ID
- **userId** (required) in path: Instagram-scoped user id (IGSID) from a webhook payload
- **refresh** (optional) in query: Bypass the cache and re-query Meta
### Responses
#### 200: Follow status (fields are null when Meta would not resolve it)
**Response Body:**
- **userId** (required) `string`: No description
- **accountId** (required) `string`: No description
- **isFollower** (required) `boolean,null`: The user follows this account. Null = unknown, never "no".
- **isFollowedByAccount** `boolean,null`: This account follows the user.
- **followerCount** `integer,null`: No description
- **isVerified** `boolean,null`: No description
- **username** `string,null`: No description
- **name** `string,null`: No description
- **unavailableReason** `string,null`: Why the follow relationship could not be resolved. Null when it was. - one of: consent_required, dm_access_disabled, not_messageable, error,
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: Resource not found
**Response Body:**
- **error** `string`: No description (example: "Not found")
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Get Slack account settings API Reference
Returns the connected Slack channel details and the default message identity (name and avatar shown as the author on every post, with Slack's APP badge). The identity applies to messages only; the app's own Slack profile is global and cannot be changed per workspace.
## GET /v1/accounts/{accountId}/slack-settings
**Get Slack account settings**
Returns the connected Slack channel details and the default message identity (name and avatar shown as the author on every post, with Slack's APP badge). The identity applies to messages only; the app's own Slack profile is global and cannot be changed per workspace.
### Parameters
- **accountId** (required) in path: No description
### Responses
#### 200: Slack account settings
**Response Body:**
- **account** `object`:
- **_id** `string`: No description
- **platform** `string`: No description (example: "slack")
- **displayName** `string,null`: No description
- **channelId** `string,null`: No description
- **channelName** `string,null`: No description
- **channelType** `string,null`: public or private
- **teamId** `string,null`: No description
- **teamName** `string,null`: No description
- **defaultUsername** `string,null`: No description
- **defaultIconUrl** `string,null`: No description
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: Account not found
---
## PATCH /v1/accounts/{accountId}/slack-settings
**Update Slack account settings**
Set or clear the default message identity for this channel. Empty string clears a field; per-post platformSpecificData.username/iconUrl still override these defaults.
### Parameters
- **accountId** (required) in path: No description
### Request Body
- **defaultUsername** `string`: Author name shown on posts. Empty string clears it.
- **defaultIconUrl** `string`: Author avatar image URL. Empty string clears it.
### Responses
#### 200: Updated settings
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: Account not found
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Get TikTok creator info API Reference
Returns TikTok creator details, available privacy levels, posting limits, and commercial content options for a specific TikTok account. Only works with TikTok accounts.
## GET /v1/accounts/{accountId}/tiktok/creator-info
**Get TikTok creator info**
Returns TikTok creator details, available privacy levels, posting limits, and commercial content options for a specific TikTok account. Only works with TikTok accounts.
### Parameters
- **accountId** (required) in path: The TikTok account ID
- **mediaType** (optional) in query: The media type to get creator info for (affects available interaction settings)
### Responses
#### 200: TikTok creator info and posting options
**Response Body:**
- **creator** `object`:
- **nickname** `string`: Creator display name
- **avatarUrl** `string`: Creator avatar URL
- **isVerified** `boolean`: Whether the creator is verified
- **canPostMore** `boolean`: Whether the creator can publish more posts right now
- **privacyLevels** `array[object]`: Available privacy level options for this creator
- **value** `string`: Privacy level value to use when creating posts (e.g. PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, SELF_ONLY)
- **label** `string`: Human-readable label
- **postingLimits** `object`:
- **maxVideoDurationSec** `integer`: Maximum video duration in seconds
- **interactionSettings** `object`: Per-interaction descriptors for the comment, duet and stitch toggles. Each key matches the tiktokSettings field of the same name on the create-post request. allow_duet and allow_stitch are null when mediaType is photo, because TikTok does not apply duet or stitch to photo posts.
- **allow_comment** `object`: Descriptor for the allow_comment toggle.
- **enabled** `boolean`: Whether the creator permits this interaction. False means they disabled it in the TikTok app. This is availability, never the value the user selected.
- **required** `boolean`: Whether tiktokSettings.allow_comment must be supplied when creating a post. Always true, because TikTok forbids defaulting it.
- **default** `boolean`: Initial value a post composer should render. A UI seed only, never applied server-side when the field is omitted.
- **label** `string`: Human-readable toggle label.
- **allow_duet** `object,null`: Descriptor for the allow_duet toggle. Null when mediaType is photo.
- **allow_stitch** `object,null`: Descriptor for the allow_stitch toggle. Null when mediaType is photo.
- **commercialContentTypes** `array[object]`: Available commercial content disclosure options
- **value** `string`: No description
- **label** `string`: No description
- **requires** `array[string]`:
#### 400: Account is not a TikTok account
**Response Body:**
- **error** `string`: No description
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: Resource not found
**Response Body:**
- **error** `string`: No description (example: "Not found")
#### 429: Creator has reached TikTok daily posting limit
**Response Body:**
- **error** `string`: No description
---
---
# List accounts API Reference
Returns connected accounts. Only includes accounts within the plan limit by default. Follower data requires analytics add-on.
Supports optional server-side pagination via page/limit params. When omitted, returns all accounts (backward-compatible).
page and limit must be supplied together; out-of-range page/limit values are rejected with 400 rather than silently clamped.
## GET /v1/accounts
**List accounts**
Returns connected accounts. Only includes accounts within the plan limit by default. Follower data requires analytics add-on.
Supports optional server-side pagination via page/limit params. When omitted, returns all accounts (backward-compatible).
page and limit must be supplied together; out-of-range page/limit values are rejected with 400 rather than silently clamped.
### Parameters
- **profileId** (optional) in query: Filter accounts by profile ID. Must be a valid ObjectId.
- **platform** (optional) in query: Filter accounts by platform (e.g. "instagram", "twitter").
- **status** (optional) in query: Filter accounts by connection status. `connected` returns healthy accounts; `disconnected` returns accounts that need reconnection (per the same reconnection check surfaced in the dashboard). Omit to return accounts in any status. When combined with page/limit, pagination totals reflect the filtered result set.
- **includeOverLimit** (optional) in query: When true, includes accounts from over-limit profiles.
- **page** (optional) in query: Page number (1-based). Must be provided together with limit to enable server-side pagination; sending only one of the two returns 400. Omit both for all accounts.
- **limit** (optional) in query: Page size. Must be provided together with page; sending only one of the two returns 400.
### Responses
#### 200: Accounts (with optional pagination)
**Response Body:**
- **accounts** (required) `array[SocialAccount]`:
- **hasAnalyticsAccess** (required) `boolean`: Whether user has analytics add-on access
- **pagination**: `Pagination` - Only present when page/limit params are provided
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 503: An upstream service or database is temporarily unavailable. Retry after the indicated delay. A timed-out write may have completed upstream; check its outcome before resubmitting.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Related Schema Definitions
## AccountsListResponse
### Properties
- **accounts** (required) `array`: No description
- **hasAnalyticsAccess** (required) `boolean`: Whether user has analytics add-on access
- **pagination**: Only present when page/limit params are provided
## SocialAccount
### Properties
- **_id** (required) `string`: No description
- **platform** (required) `string`: No description - one of: tiktok, instagram, facebook, youtube, linkedin, twitter, threads, pinterest, reddit, bluesky, googlebusiness, telegram, snapchat, discord, slack, whatsapp, linkedinads, metaads, pinterestads, tiktokads, xads, googleads, openaiads, sms, phone, rcs
- **profileId** (required): No description
- **username** `string`: No description
- **displayName** `string`: No description
- **profilePicture** `string,null`: URL to the account's profile picture on the platform. May be null if the platform does not provide one.
- **profileUrl** `string`: Full profile URL for the connected account on its platform.
- **isActive** (required) `boolean`: No description
- **needsReconnection** `boolean`: The platform definitively reported the stored OAuth token as dead.
While true, GET /v1/connect/{platform}/ads returns a
fresh authUrl (implicit force=true) instead of alreadyConnected,
so re-running the connect flow recovers the account. Cleared
automatically when the account is re-authorized.
- **followersCount** `number`: Follower count (only included if user has analytics add-on)
- **followersLastUpdated** `string`: Last time follower count was updated (only included if user has analytics add-on)
- **parentAccountId** `string,null`: Reference to the parent posting SocialAccount. Set for ads accounts that share
or derive from a posting account's OAuth token. null for standalone ads (Google Ads)
and all posting accounts. Meta ads business-login accounts also have no parent.
- **enabled** `boolean`: Whether the user explicitly activated this account. false means the account was
created as a side effect (e.g., posting account auto-created when user connected
ads first). Such accounts are hidden from this list, cannot be posted to
(`ACCOUNT_NOT_ENABLED_FOR_POSTING`), and are not billed as connected accounts.
- **metadata** `object`: Platform-specific metadata. Fields vary by platform. For WhatsApp accounts, includes:
- qualityRating: Phone number quality rating from Meta (GREEN, YELLOW, RED, or UNKNOWN)
- nameStatus: Display name review status (APPROVED, PENDING_REVIEW, DECLINED, or NONE). A declined or pending display name does not by itself block sending; sendability is reported separately via health_status (can_send_message).
- messagingLimitTier: Maximum unique business-initiated conversations per 24h rolling window (TIER_250, TIER_1K, TIER_10K, TIER_100K, or TIER_UNLIMITED). Scales automatically as quality rating improves.
- verifiedName: Meta-verified business display name
- displayPhoneNumber: Formatted phone number (e.g., "+1 555-123-4567")
- wabaId: WhatsApp Business Account ID
- phoneNumberId: Meta phone number ID
For Meta ads business-login accounts:
- tokenType: system-user
- businessId: The owning Business Manager ID when there is one owner; null for multiple owners.
- businessIds: Owning Business Manager IDs discovered from granted ad accounts.
- grantedAdAccountIds: Ad-account IDs granted to the token.
- adAccountBusinesses: Map from ad-account ID to its owning business ID or null.
- availablePages: Granted Page IDs and names. No Page tokens are exposed.
- selectedPageId: The Page selected for creatives and lead forms, or null.
- scopedAdAccountIds: Existing sync scope preserved on reconnect.
Non-expiring tokens have no tokenExpiresAt field. Parent posting reconnects do not replace this token.
For LinkedIn accounts, profileData carries the profile details refreshed on each daily snapshot:
- profileData.bio: The member's headline for personal accounts, or the organization description for organization accounts. null when the member has not set one.
- profileData.extraData.vanityName: The member's profile slug, i.e. the /in/{vanityName} segment of profileUrl. Personal accounts only; an organization's own slug is in metadata.organizationInfo.vanityName.
## Pagination
### Properties
- **page** `integer`: No description
- **limit** `integer`: No description
- **total** `integer`: No description
- **pages** `integer`: No description
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# List trending commercial music API Reference
Returns the 100 currently trending tracks of TikTok's Commercial Music Library for a TikTok account connected through the TikTok for Business app. Use a track id as tiktokSettings.musicSoundInfo.musicSoundId when creating a post. The list is not paged; countryCode selects the country chart.
## GET /v1/accounts/{accountId}/tiktok/commercial-music
**List trending commercial music**
Returns the 100 currently trending tracks of TikTok's Commercial Music Library for a TikTok account connected through the TikTok for Business app. Use a track id as tiktokSettings.musicSoundInfo.musicSoundId when creating a post. The list is not paged; countryCode selects the country chart.
### Parameters
- **accountId** (required) in path: The TikTok account ID
- **countryCode** (optional) in query: Two-letter ISO 3166-1 country code of the chart to read (for example ES). Defaults to TikTok's global chart.
### Responses
#### 200: The trending tracks, rank 1 first
**Response Body:**
- **tracks** `array[object]`:
- **id** `string`: The id to send as musicSoundId (the full track's song clip id). TikTok rejects the commercial music id itself at publish time.
- **commercialMusicId** `string`: TikTok's commercial_music_id, for reference only
- **name** `string`: No description
- **artist** `string`: No description
- **durationSec** `integer`: No description
- **genres** `array[string]`:
- **previewUrl** `string`: Preview audio of the full track
- **thumbnailUrl** `string`: No description
- **rank** `integer`: Position in the trending chart, 1 first
- **clip** `object`: The trending excerpt of the track, when TikTok provides one. Its id is also accepted as musicSoundId.
- **id** `string`: No description
- **durationSec** `integer`: No description
- **previewUrl** `string`: No description
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 404: Account not found
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Move account to another profile API Reference
Moves a connected account to a different profile owned by the same
user. The target profile must belong to the same user as the account.
For API keys restricted to specific profiles, BOTH the source account's
current profile AND the target profile must be in the key's allowed set.
Calls with a target profile outside the key's scope return 403.
## PUT /v1/accounts/{accountId}
**Update account**
Updates a connected account's display name or username override.
For X accounts on usage-based billing, also accepts an `xCapabilities`
object to toggle background API operations that incur X API pass-through costs.
Both fields are opt-in (default `false`). When off, no analytics syncs or DM
polling are performed for that account, and no API call is metered for those
operations. Publishing and deleting posts are always available regardless of
these toggles. Setting `xCapabilities` on a non-X account returns 400.
### Parameters
- **accountId** (required) in path: No description
### Request Body
- **username** `string`: No description
- **displayName** `string`: No description
- **xCapabilities** `object`: X only. Per-account opt-in toggles for background API
operations that incur X API pass-through costs. Each call is
billed at the X tier rate. Either field can be
sent independently; omitted fields are unchanged.
### Responses
#### 200: Updated
**Response Body:**
- **message** `string`: No description
- **username** `string`: No description
- **displayName** `string`: No description
- **xCapabilities** `object`: Echo of the resulting `xCapabilities` state, returned only
when the request body included an `xCapabilities` object.
- **analytics** `boolean`: No description
- **inbox** `boolean`: No description
#### 400: Invalid request (e.g. xCapabilities on a non-X account)
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: Resource not found
**Response Body:**
- **error** `string`: No description (example: "Not found")
---
## PATCH /v1/accounts/{accountId}
**Move account to another profile**
Moves a connected account to a different profile owned by the same
user. The target profile must belong to the same user as the account.
For API keys restricted to specific profiles, BOTH the source account's
current profile AND the target profile must be in the key's allowed set.
Calls with a target profile outside the key's scope return 403.
### Parameters
- **accountId** (required) in path: No description
### Request Body
- **profileId** (required) `string`: Target profile ID (must be a valid ObjectId and owned by the same user as the account).
### Responses
#### 200: Account moved
**Response Body:**
- **message** `string`: No description
- **profileId** `string`: No description
#### 400: Missing or invalid profileId
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: API key does not have access to the source account or target profile
#### 404: Account or target profile not found
---
## DELETE /v1/accounts/{accountId}
**Disconnect account**
Disconnects and removes a connected account. Repeating the call for an account already disconnected returns 404, the account stays in its 1h grace window and the disconnect is not re-run.
### Parameters
- **accountId** (required) in path: No description
### Responses
#### 200: Disconnected
**Response Body:**
- **message** `string`: No description
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: Resource not found
**Response Body:**
- **error** `string`: No description (example: "Not found")
---
---
# Update account API Reference
Updates a connected account's display name or username override.
For X accounts on usage-based billing, also accepts an `xCapabilities`
object to toggle background API operations that incur X API pass-through costs.
Both fields are opt-in (default `false`). When off, no analytics syncs or DM
polling are performed for that account, and no API call is metered for those
operations. Publishing and deleting posts are always available regardless of
these toggles. Setting `xCapabilities` on a non-X account returns 400.
## PUT /v1/accounts/{accountId}
**Update account**
Updates a connected account's display name or username override.
For X accounts on usage-based billing, also accepts an `xCapabilities`
object to toggle background API operations that incur X API pass-through costs.
Both fields are opt-in (default `false`). When off, no analytics syncs or DM
polling are performed for that account, and no API call is metered for those
operations. Publishing and deleting posts are always available regardless of
these toggles. Setting `xCapabilities` on a non-X account returns 400.
### Parameters
- **accountId** (required) in path: No description
### Request Body
- **username** `string`: No description
- **displayName** `string`: No description
- **xCapabilities** `object`: X only. Per-account opt-in toggles for background API
operations that incur X API pass-through costs. Each call is
billed at the X tier rate. Either field can be
sent independently; omitted fields are unchanged.
### Responses
#### 200: Updated
**Response Body:**
- **message** `string`: No description
- **username** `string`: No description
- **displayName** `string`: No description
- **xCapabilities** `object`: Echo of the resulting `xCapabilities` state, returned only
when the request body included an `xCapabilities` object.
- **analytics** `boolean`: No description
- **inbox** `boolean`: No description
#### 400: Invalid request (e.g. xCapabilities on a non-X account)
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: Resource not found
**Response Body:**
- **error** `string`: No description (example: "Not found")
---
## PATCH /v1/accounts/{accountId}
**Move account to another profile**
Moves a connected account to a different profile owned by the same
user. The target profile must belong to the same user as the account.
For API keys restricted to specific profiles, BOTH the source account's
current profile AND the target profile must be in the key's allowed set.
Calls with a target profile outside the key's scope return 403.
### Parameters
- **accountId** (required) in path: No description
### Request Body
- **profileId** (required) `string`: Target profile ID (must be a valid ObjectId and owned by the same user as the account).
### Responses
#### 200: Account moved
**Response Body:**
- **message** `string`: No description
- **profileId** `string`: No description
#### 400: Missing or invalid profileId
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: API key does not have access to the source account or target profile
#### 404: Account or target profile not found
---
## DELETE /v1/accounts/{accountId}
**Disconnect account**
Disconnects and removes a connected account. Repeating the call for an account already disconnected returns 404, the account stays in its 1h grace window and the disconnect is not re-run.
### Parameters
- **accountId** (required) in path: No description
### Responses
#### 200: Disconnected
**Response Body:**
- **message** `string`: No description
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: Resource not found
**Response Body:**
- **error** `string`: No description (example: "Not found")
---
---
# Update Bluesky account settings API Reference
Set or clear the account's default post languages. 1-3 BCP-47 codes (e.g. "pt", "en-US"), the same validation as per-post langs; explicit null clears the default. Per-post platformSpecificData.langs always overrides this default. Applies to posts published after the change; already-published posts cannot be retagged (Bluesky has no post edit).
## GET /v1/accounts/{accountId}/bluesky-settings
**Get Bluesky account settings**
Returns the account's default post languages (defaultLangs), applied at publish time whenever a post's platformSpecificData.langs is absent. Null when no default is set.
### Parameters
- **accountId** (required) in path: No description
### Responses
#### 200: Bluesky account settings
**Response Body:**
- **defaultLangs** `array,null`: No description (example: ["pt"])
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: Account not found
---
## PATCH /v1/accounts/{accountId}/bluesky-settings
**Update Bluesky account settings**
Set or clear the account's default post languages. 1-3 BCP-47 codes (e.g. "pt", "en-US"), the same validation as per-post langs; explicit null clears the default. Per-post platformSpecificData.langs always overrides this default. Applies to posts published after the change; already-published posts cannot be retagged (Bluesky has no post edit).
### Parameters
- **accountId** (required) in path: No description
### Request Body
- **defaultLangs** (required) `array,null`: No description
### Responses
#### 200: Updated settings
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: Account not found
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Update Slack account settings API Reference
Set or clear the default message identity for this channel. Empty string clears a field; per-post platformSpecificData.username/iconUrl still override these defaults.
## GET /v1/accounts/{accountId}/slack-settings
**Get Slack account settings**
Returns the connected Slack channel details and the default message identity (name and avatar shown as the author on every post, with Slack's APP badge). The identity applies to messages only; the app's own Slack profile is global and cannot be changed per workspace.
### Parameters
- **accountId** (required) in path: No description
### Responses
#### 200: Slack account settings
**Response Body:**
- **account** `object`:
- **_id** `string`: No description
- **platform** `string`: No description (example: "slack")
- **displayName** `string,null`: No description
- **channelId** `string,null`: No description
- **channelName** `string,null`: No description
- **channelType** `string,null`: public or private
- **teamId** `string,null`: No description
- **teamName** `string,null`: No description
- **defaultUsername** `string,null`: No description
- **defaultIconUrl** `string,null`: No description
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: Account not found
---
## PATCH /v1/accounts/{accountId}/slack-settings
**Update Slack account settings**
Set or clear the default message identity for this channel. Empty string clears a field; per-post platformSpecificData.username/iconUrl still override these defaults.
### Parameters
- **accountId** (required) in path: No description
### Request Body
- **defaultUsername** `string`: Author name shown on posts. Empty string clears it.
- **defaultIconUrl** `string`: Author avatar image URL. Empty string clears it.
### Responses
#### 200: Updated settings
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: Account not found
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Add users to audience API Reference
Upload user data to a customer_list audience. Data is SHA256-hashed server-side before sending to the platform.
Email is used on every platform; phone is used on Meta only (other platforms ignore it). On TikTok and Pinterest,
the first upload also provisions the audience (deferred create). LinkedIn uploads are full-replace. Max 10,000 users per request.
customer_list only. A LinkedIn `company_list` audience takes company rows, not people: send those to
`POST /v1/ads/audiences/{audienceId}/companies`. This endpoint 422s for every other audience type.
## POST /v1/ads/audiences/{audienceId}/users
**Add users to audience**
Upload user data to a customer_list audience. Data is SHA256-hashed server-side before sending to the platform.
Email is used on every platform; phone is used on Meta only (other platforms ignore it). On TikTok and Pinterest,
the first upload also provisions the audience (deferred create). LinkedIn uploads are full-replace. Max 10,000 users per request.
customer_list only. A LinkedIn `company_list` audience takes company rows, not people: send those to
`POST /v1/ads/audiences/{audienceId}/companies`. This endpoint 422s for every other audience type.
### Parameters
- **audienceId** (required) in path: The Zernio audience id (the id field of GET /v1/ads/audiences), not the platform segment id.
### Request Body
- **users** (required) `array`: No description
### Responses
#### 200: Users added
**Response Body:**
- **message** `string`: No description
- **numReceived** `integer`: No description
- **numInvalid** `integer`: No description
#### 400: Invalid input (malformed audienceId, empty users array, missing email/phone)
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access required. Legacy plans need the Ads add-on; included by default on usage-based plans.
#### 404: Resource not found
**Response Body:**
- **error** `string`: No description (example: "Not found")
#### 422: Audience is not a customer_list type or has no platform ID yet
---
---
# Create custom audience API Reference
Create a custom audience. `customer_list` is supported on Meta, Google, X, LinkedIn, TikTok, and Pinterest;
`website` and `lookalike` are Meta-only; `company_list`, `engagement` and `website_retargeting` are LinkedIn-only.
`saved_targeting` stores a reusable TargetingSpec (no member upload, no adAccountId) that you reference later via
`savedTargetingId` on `POST /v1/ads/create`.
How the audience gets filled depends on the type:
- `customer_list` is created empty. Add members with `POST /v1/ads/audiences/{audienceId}/users`.
On TikTok and Pinterest the audience is provisioned lazily on that first upload (until then its status is `pending`).
- `company_list` is filled AT CREATION from the `companies` array below, which is required. To change the list
afterwards send the new full list to `POST /v1/ads/audiences/{audienceId}/companies` (a replace, not a merge).
The `/users` endpoint rejects these audiences with a 422.
- `website`, `website_retargeting`, `engagement`, `meta_engagement` and `lookalike` fill themselves from the pixel,
engagement source or seed audience you point them at. They take no member upload at all.
Create is not idempotent, never auto-retry.
## GET /v1/ads/audiences
**List custom audiences**
Returns custom audiences for the given ad account. Supports Meta, Google, TikTok, Pinterest, LinkedIn, and X.
### Parameters
- **accountId** (required) in query: Account ID
- **adAccountId** (required) in query: Platform ad account ID
- **platform** (optional) in query: No description
- **type** (optional) in query: Filter to one audience type. `saved_targeting` returns stored TargetingSpec audiences; the other types return uploaded/derived audiences.
### Responses
#### 200: Audiences
**Response Body:**
- **audiences** `array[object]`:
- **id** `string,null`: The Zernio audience id. Pass this as audienceId on GET /v1/ads/audiences/{audienceId} and the companies/users upload endpoints. Null when the audience was not created through Zernio.
- **accountId** `string`: Account the audience was created against. Returned for saved_targeting items.
- **platformAudienceId** `string`: No description
- **name** `string`: No description
- **description** `string`: No description
- **type** `string`: No description - one of: customer_list, company_list, engagement, meta_engagement, website, website_retargeting, lookalike, saved_targeting
- **spec**: Present (and the only meaningful payload) when `type` is `saved_targeting`. Null for uploaded/derived audience types.
- **platform** `string`: No description
- **size** `integer`: No description
- **status** `string`: No description
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access required. Legacy plans need the Ads add-on; included by default on usage-based plans.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
## POST /v1/ads/audiences
**Create custom audience**
Create a custom audience. `customer_list` is supported on Meta, Google, X, LinkedIn, TikTok, and Pinterest;
`website` and `lookalike` are Meta-only; `company_list`, `engagement` and `website_retargeting` are LinkedIn-only.
`saved_targeting` stores a reusable TargetingSpec (no member upload, no adAccountId) that you reference later via
`savedTargetingId` on `POST /v1/ads/create`.
How the audience gets filled depends on the type:
- `customer_list` is created empty. Add members with `POST /v1/ads/audiences/{audienceId}/users`.
On TikTok and Pinterest the audience is provisioned lazily on that first upload (until then its status is `pending`).
- `company_list` is filled AT CREATION from the `companies` array below, which is required. To change the list
afterwards send the new full list to `POST /v1/ads/audiences/{audienceId}/companies` (a replace, not a merge).
The `/users` endpoint rejects these audiences with a 422.
- `website`, `website_retargeting`, `engagement`, `meta_engagement` and `lookalike` fill themselves from the pixel,
engagement source or seed audience you point them at. They take no member upload at all.
Create is not idempotent, never auto-retry.
### Request Body
- **platformSpecificData**: Platform-specific settings (see schema definitions below)
### Responses
#### 201: Audience created
**Response Body:**
- **audience** `object`: No description
- **message** `string`: No description
#### 400: Missing required fields
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access required. Legacy plans need the Ads add-on; included by default on usage-based plans.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Delete custom audience API Reference
Deletes the audience from both the platform and the local database. `saved_targeting` audiences exist only on Zernio, so only the local record is removed.
## GET /v1/ads/audiences/{audienceId}
**Get audience details**
Returns the local audience record and fresh data from Meta (if available).
### Parameters
- **audienceId** (required) in path: The Zernio audience id (the id field of GET /v1/ads/audiences), not the platform segment id.
### Responses
#### 200: Audience details
**Response Body:**
- **audience** `object`: No description
- **platformData** `object,null`: Fresh data from the platform API
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access required. Legacy plans need the Ads add-on; included by default on usage-based plans.
#### 404: Resource not found
**Response Body:**
- **error** `string`: No description (example: "Not found")
---
## PUT /v1/ads/audiences/{audienceId}
**Update an audience**
Update an audience. `saved_targeting` audiences accept `name`, `description`, and `spec`
(full replacement, no merge, Zernio-only, no platform call). Platform audiences
(uploaded/website/lookalike) accept `name` and `description` only, updated on the
platform first and then mirrored locally; their rules are immutable, so `spec` returns
400 for them. Platform audience updates are Meta-only for now (other platforms return
501). Ads already created from a saved_targeting audience are unaffected, they snapshot
the targeting at creation.
### Parameters
- **audienceId** (required) in path: No description
### Request Body
- **name** `string`: No description
- **description** `string`: No description
- **spec**: Full replacement for the stored targeting spec.
### Responses
#### 200: Audience updated
**Response Body:**
- **audience** `object`: No description
- **message** `string`: No description
#### 400: Invalid body (no fields provided, malformed spec, or spec on a platform audience)
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access required. Legacy plans need the Ads add-on; included by default on usage-based plans.
#### 404: Resource not found
**Response Body:**
- **error** `string`: No description (example: "Not found")
#### 422: The audience has no platform counterpart to update
#### 501: Platform audience updates are only supported on Meta
---
## DELETE /v1/ads/audiences/{audienceId}
**Delete custom audience**
Deletes the audience from both the platform and the local database. `saved_targeting` audiences exist only on Zernio, so only the local record is removed.
### Parameters
- **audienceId** (required) in path: No description
### Responses
#### 200: Audience deleted
**Response Body:**
- **message** `string`: No description
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access required. Legacy plans need the Ads add-on; included by default on usage-based plans.
#### 404: Resource not found
**Response Body:**
- **error** `string`: No description (example: "Not found")
---
---
# Get audience details API Reference
Returns the local audience record and fresh data from Meta (if available).
## GET /v1/ads/audiences/{audienceId}
**Get audience details**
Returns the local audience record and fresh data from Meta (if available).
### Parameters
- **audienceId** (required) in path: The Zernio audience id (the id field of GET /v1/ads/audiences), not the platform segment id.
### Responses
#### 200: Audience details
**Response Body:**
- **audience** `object`: No description
- **platformData** `object,null`: Fresh data from the platform API
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access required. Legacy plans need the Ads add-on; included by default on usage-based plans.
#### 404: Resource not found
**Response Body:**
- **error** `string`: No description (example: "Not found")
---
## PUT /v1/ads/audiences/{audienceId}
**Update an audience**
Update an audience. `saved_targeting` audiences accept `name`, `description`, and `spec`
(full replacement, no merge, Zernio-only, no platform call). Platform audiences
(uploaded/website/lookalike) accept `name` and `description` only, updated on the
platform first and then mirrored locally; their rules are immutable, so `spec` returns
400 for them. Platform audience updates are Meta-only for now (other platforms return
501). Ads already created from a saved_targeting audience are unaffected, they snapshot
the targeting at creation.
### Parameters
- **audienceId** (required) in path: No description
### Request Body
- **name** `string`: No description
- **description** `string`: No description
- **spec**: Full replacement for the stored targeting spec.
### Responses
#### 200: Audience updated
**Response Body:**
- **audience** `object`: No description
- **message** `string`: No description
#### 400: Invalid body (no fields provided, malformed spec, or spec on a platform audience)
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access required. Legacy plans need the Ads add-on; included by default on usage-based plans.
#### 404: Resource not found
**Response Body:**
- **error** `string`: No description (example: "Not found")
#### 422: The audience has no platform counterpart to update
#### 501: Platform audience updates are only supported on Meta
---
## DELETE /v1/ads/audiences/{audienceId}
**Delete custom audience**
Deletes the audience from both the platform and the local database. `saved_targeting` audiences exist only on Zernio, so only the local record is removed.
### Parameters
- **audienceId** (required) in path: No description
### Responses
#### 200: Audience deleted
**Response Body:**
- **message** `string`: No description
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access required. Legacy plans need the Ads add-on; included by default on usage-based plans.
#### 404: Resource not found
**Response Body:**
- **error** `string`: No description (example: "Not found")
---
---
# List custom audiences API Reference
Returns custom audiences for the given ad account. Supports Meta, Google, TikTok, Pinterest, LinkedIn, and X.
## GET /v1/ads/audiences
**List custom audiences**
Returns custom audiences for the given ad account. Supports Meta, Google, TikTok, Pinterest, LinkedIn, and X.
### Parameters
- **accountId** (required) in query: Account ID
- **adAccountId** (required) in query: Platform ad account ID
- **platform** (optional) in query: No description
- **type** (optional) in query: Filter to one audience type. `saved_targeting` returns stored TargetingSpec audiences; the other types return uploaded/derived audiences.
### Responses
#### 200: Audiences
**Response Body:**
- **audiences** `array[object]`:
- **id** `string,null`: The Zernio audience id. Pass this as audienceId on GET /v1/ads/audiences/{audienceId} and the companies/users upload endpoints. Null when the audience was not created through Zernio.
- **accountId** `string`: Account the audience was created against. Returned for saved_targeting items.
- **platformAudienceId** `string`: No description
- **name** `string`: No description
- **description** `string`: No description
- **type** `string`: No description - one of: customer_list, company_list, engagement, meta_engagement, website, website_retargeting, lookalike, saved_targeting
- **spec**: Present (and the only meaningful payload) when `type` is `saved_targeting`. Null for uploaded/derived audience types.
- **platform** `string`: No description
- **size** `integer`: No description
- **status** `string`: No description
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access required. Legacy plans need the Ads add-on; included by default on usage-based plans.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
## POST /v1/ads/audiences
**Create custom audience**
Create a custom audience. `customer_list` is supported on Meta, Google, X, LinkedIn, TikTok, and Pinterest;
`website` and `lookalike` are Meta-only; `company_list`, `engagement` and `website_retargeting` are LinkedIn-only.
`saved_targeting` stores a reusable TargetingSpec (no member upload, no adAccountId) that you reference later via
`savedTargetingId` on `POST /v1/ads/create`.
How the audience gets filled depends on the type:
- `customer_list` is created empty. Add members with `POST /v1/ads/audiences/{audienceId}/users`.
On TikTok and Pinterest the audience is provisioned lazily on that first upload (until then its status is `pending`).
- `company_list` is filled AT CREATION from the `companies` array below, which is required. To change the list
afterwards send the new full list to `POST /v1/ads/audiences/{audienceId}/companies` (a replace, not a merge).
The `/users` endpoint rejects these audiences with a 422.
- `website`, `website_retargeting`, `engagement`, `meta_engagement` and `lookalike` fill themselves from the pixel,
engagement source or seed audience you point them at. They take no member upload at all.
Create is not idempotent, never auto-retry.
### Request Body
- **platformSpecificData**: Platform-specific settings (see schema definitions below)
### Responses
#### 201: Audience created
**Response Body:**
- **audience** `object`: No description
- **message** `string`: No description
#### 400: Missing required fields
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access required. Legacy plans need the Ads add-on; included by default on usage-based plans.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Replace audience companies API Reference
Upload the company rows of a LinkedIn `company_list` audience (account-based marketing).
LinkedIn-only, every other platform returns 422.
A LinkedIn audience segment holds exactly one uploaded list, so the list you send here
REPLACES the segment's list instead of being appended to it: always send the full set of
companies. LinkedIn returns only the identifier of the uploaded file, never its rows, so the
merge cannot be done for you, keep the source list on your side.
How the matching behaves:
- Rows are plain text (not hashed), matched against LinkedIn's own company graph.
- Matching is asynchronous: LinkedIn takes up to 48h for a new audience and up to 24h for a
later update, and the audience stays `processing` meanwhile.
- LinkedIn does not document how quickly companies dropped from the list stop being targeted,
so treat removals as eventual rather than immediate.
- LinkedIn recommends at least 1,000 companies for a usable match rate, and caps a list at
300,000.
The initial list is sent with `companies` on `POST /v1/ads/audiences`; this endpoint is for
every change after that.
## POST /v1/ads/audiences/{audienceId}/companies
**Replace audience companies**
Upload the company rows of a LinkedIn `company_list` audience (account-based marketing).
LinkedIn-only, every other platform returns 422.
A LinkedIn audience segment holds exactly one uploaded list, so the list you send here
REPLACES the segment's list instead of being appended to it: always send the full set of
companies. LinkedIn returns only the identifier of the uploaded file, never its rows, so the
merge cannot be done for you, keep the source list on your side.
How the matching behaves:
- Rows are plain text (not hashed), matched against LinkedIn's own company graph.
- Matching is asynchronous: LinkedIn takes up to 48h for a new audience and up to 24h for a
later update, and the audience stays `processing` meanwhile.
- LinkedIn does not document how quickly companies dropped from the list stop being targeted,
so treat removals as eventual rather than immediate.
- LinkedIn recommends at least 1,000 companies for a usable match rate, and caps a list at
300,000.
The initial list is sent with `companies` on `POST /v1/ads/audiences`; this endpoint is for
every change after that.
### Parameters
- **audienceId** (required) in path: The Zernio audience id (the id field of GET /v1/ads/audiences), not the platform segment id.
### Request Body
- **companies** (required) `array`: The complete company list. Each row needs at least one of name, domain, website or linkedinPageUrl.
### Responses
#### 200: Companies uploaded
**Response Body:**
- **message** `string`: No description
- **numReceived** `integer`: Rows sent to LinkedIn. Matching happens asynchronously, so this is not the matched company count.
#### 400: Invalid input (malformed audienceId, empty companies array, a row with no identifier)
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access required. Legacy plans need the Ads add-on; included by default on usage-based plans.
#### 404: Resource not found
**Response Body:**
- **error** `string`: No description (example: "Not found")
#### 422: Audience is not a company_list type, is not on LinkedIn, or has no platform ID yet
---
---
# Update an audience API Reference
Update an audience. `saved_targeting` audiences accept `name`, `description`, and `spec`
(full replacement, no merge, Zernio-only, no platform call). Platform audiences
(uploaded/website/lookalike) accept `name` and `description` only, updated on the
platform first and then mirrored locally; their rules are immutable, so `spec` returns
400 for them. Platform audience updates are Meta-only for now (other platforms return
501). Ads already created from a saved_targeting audience are unaffected, they snapshot
the targeting at creation.
## GET /v1/ads/audiences/{audienceId}
**Get audience details**
Returns the local audience record and fresh data from Meta (if available).
### Parameters
- **audienceId** (required) in path: The Zernio audience id (the id field of GET /v1/ads/audiences), not the platform segment id.
### Responses
#### 200: Audience details
**Response Body:**
- **audience** `object`: No description
- **platformData** `object,null`: Fresh data from the platform API
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access required. Legacy plans need the Ads add-on; included by default on usage-based plans.
#### 404: Resource not found
**Response Body:**
- **error** `string`: No description (example: "Not found")
---
## PUT /v1/ads/audiences/{audienceId}
**Update an audience**
Update an audience. `saved_targeting` audiences accept `name`, `description`, and `spec`
(full replacement, no merge, Zernio-only, no platform call). Platform audiences
(uploaded/website/lookalike) accept `name` and `description` only, updated on the
platform first and then mirrored locally; their rules are immutable, so `spec` returns
400 for them. Platform audience updates are Meta-only for now (other platforms return
501). Ads already created from a saved_targeting audience are unaffected, they snapshot
the targeting at creation.
### Parameters
- **audienceId** (required) in path: No description
### Request Body
- **name** `string`: No description
- **description** `string`: No description
- **spec**: Full replacement for the stored targeting spec.
### Responses
#### 200: Audience updated
**Response Body:**
- **audience** `object`: No description
- **message** `string`: No description
#### 400: Invalid body (no fields provided, malformed spec, or spec on a platform audience)
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access required. Legacy plans need the Ads add-on; included by default on usage-based plans.
#### 404: Resource not found
**Response Body:**
- **error** `string`: No description (example: "Not found")
#### 422: The audience has no platform counterpart to update
#### 501: Platform audience updates are only supported on Meta
---
## DELETE /v1/ads/audiences/{audienceId}
**Delete custom audience**
Deletes the audience from both the platform and the local database. `saved_targeting` audiences exist only on Zernio, so only the local record is removed.
### Parameters
- **audienceId** (required) in path: No description
### Responses
#### 200: Audience deleted
**Response Body:**
- **message** `string`: No description
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access required. Legacy plans need the Ads add-on; included by default on usage-based plans.
#### 404: Resource not found
**Response Body:**
- **error** `string`: No description (example: "Not found")
---
---
# Add account callouts API Reference
Creates assets and customer_asset links for this Google customer. Links apply at account level.
## GET /v1/ads/accounts/callouts
**List account callouts**
Lists directly attached Google assets. Fresh reads are cached for 10 minutes; exhausted quota may return the last successful read with stale=true. Inherited assets are not included. Preserves Google RMF C.75 account-level callouts.
### Parameters
- **accountId** (required) in query: No description
- **customerId** (optional) in query: No description
### Responses
#### 200: Assets returned.
**Response Body:**
- **customerId** `string`: No description
- **callouts** `array[object]`:
- **assetId** `string`: No description
- **status** `string`: No description
- **text** `string`: No description
- **cachedAt** `string,null` (date-time): Time of the cached Google read. Null when no cache was used.
- **stale** `boolean`: True when exhausted quota required returning the last successful read.
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access is required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Only supported on Google Ads.
---
## POST /v1/ads/accounts/callouts
**Add account callouts**
Creates assets and customer_asset links for this Google customer. Links apply at account level.
### Request Body
- **accountId** (required) `string`: Zernio Google Ads connection id.
- **customerId** `string`: Google customer id without dashes. Required when the connection has multiple customers.
- **callouts** (required) `array`: No description
### Responses
#### 201: Assets created and attached.
**Response Body:**
- **customerId** `string`: No description
- **callouts** `array[object]`:
- **assetId** `string`: No description
- **text** `string`: No description
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access is required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Only supported on Google Ads.
---
## PUT /v1/ads/accounts/callouts
**Update account callouts**
Edits existing Google assets in place. Send updates with assetResourceName and the fields to change. An asset is shared: changes affect every attachment using it. Omitted fields stay unchanged. The operation consumes the Google operations budget and invalidates affected cached lists.
### Request Body
- **accountId** (required) `string`: Zernio Google Ads connection id.
- **customerId** `string`: Google customer id without dashes. Required when the connection has multiple customers.
- **updates** (required) `array`: No description
### Responses
#### 200: Assets returned.
**Response Body:**
- **updated** `integer`: No description
- **customerId** `string`: No description
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access is required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Only supported on Google Ads.
---
## DELETE /v1/ads/accounts/callouts
**Remove account callout**
Removes the customer_asset attachment only. The underlying shared asset and its campaign or ad-group attachments remain.
### Request Body
- **accountId** (required) `string`: Zernio Google Ads connection id.
- **customerId** `string`: Google customer id without dashes. Required when the connection has multiple customers.
- **assetId** (required) `string`: No description
### Responses
#### 200: Assets returned.
**Response Body:**
- **removed** `boolean`: No description
- **customerId** `string`: No description
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access is required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Only supported on Google Ads.
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Add account sitelinks API Reference
Creates assets and customer_asset links for this Google customer. Links apply at account level.
## GET /v1/ads/accounts/sitelinks
**List account sitelinks**
Lists directly attached Google assets. Fresh reads are cached for 10 minutes; exhausted quota may return the last successful read with stale=true. Inherited assets are not included.
### Parameters
- **accountId** (required) in query: No description
- **customerId** (optional) in query: No description
### Responses
#### 200: Assets returned.
**Response Body:**
- **customerId** `string`: No description
- **sitelinks** `array[object]`:
- **assetId** `string`: No description
- **status** `string`: No description
- **assetResourceName** `string`: No description
- **customerAssetResourceName** `string`: No description
- **text** `string`: No description
- **linkUrl** `string` (uri): No description
- **description1** `string`: No description
- **description2** `string`: No description
- **cachedAt** `string,null` (date-time): Time of the cached Google read. Null when no cache was used.
- **stale** `boolean`: True when exhausted quota required returning the last successful read.
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access is required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Only supported on Google Ads.
---
## POST /v1/ads/accounts/sitelinks
**Add account sitelinks**
Creates assets and customer_asset links for this Google customer. Links apply at account level.
### Request Body
- **accountId** (required) `string`: Zernio Google Ads connection id.
- **customerId** `string`: Google customer id without dashes. Required when the connection has multiple customers.
- **sitelinks** (required) `array`: No description
### Responses
#### 201: Assets created and attached.
**Response Body:**
- **customerId** `string`: No description
- **sitelinks** `array[object]`:
- **assetId** `string`: No description
- **text** `string`: No description
- **linkUrl** `string` (uri): No description
- **description1** `string`: No description
- **description2** `string`: No description
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access is required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Only supported on Google Ads.
---
## PUT /v1/ads/accounts/sitelinks
**Update account sitelinks**
Edits existing Google assets in place. Send updates with assetResourceName and the fields to change. An asset is shared: changes affect every attachment using it. Omitted fields stay unchanged. The operation consumes the Google operations budget and invalidates affected cached lists.
### Request Body
- **accountId** (required) `string`: Zernio Google Ads connection id.
- **customerId** `string`: Google customer id without dashes. Required when the connection has multiple customers.
- **updates** (required) `array`: No description
### Responses
#### 200: Assets returned.
**Response Body:**
- **updated** `integer`: No description
- **customerId** `string`: No description
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access is required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Only supported on Google Ads.
---
## DELETE /v1/ads/accounts/sitelinks
**Remove account sitelink**
Removes the customer_asset attachment only. The underlying shared asset and its campaign or ad-group attachments remain.
### Request Body
- **accountId** (required) `string`: Zernio Google Ads connection id.
- **customerId** `string`: Google customer id without dashes. Required when the connection has multiple customers.
- **assetId** (required) `string`: No description
### Responses
#### 200: Assets returned.
**Response Body:**
- **removed** `boolean`: No description
- **customerId** `string`: No description
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access is required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Only supported on Google Ads.
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Add account snippets API Reference
Creates assets and customer_asset links for this Google customer. Links apply at account level.
## GET /v1/ads/accounts/structured-snippets
**List account snippets**
Lists directly attached Google assets. Fresh reads are cached for 10 minutes; exhausted quota may return the last successful read with stale=true. Inherited assets are not included.
### Parameters
- **accountId** (required) in query: No description
- **customerId** (optional) in query: No description
### Responses
#### 200: Assets returned.
**Response Body:**
- **customerId** `string`: No description
- **structuredSnippets** `array[object]`:
- **assetId** `string`: No description
- **status** `string`: No description
- **assetResourceName** `string`: No description
- **customerAssetResourceName** `string`: No description
- **header** `string`: No description
- **values** `array[string]`:
- **cachedAt** `string,null` (date-time): Time of the cached Google read. Null when no cache was used.
- **stale** `boolean`: True when exhausted quota required returning the last successful read.
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access is required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Only supported on Google Ads.
---
## POST /v1/ads/accounts/structured-snippets
**Add account snippets**
Creates assets and customer_asset links for this Google customer. Links apply at account level.
### Request Body
- **accountId** (required) `string`: Zernio Google Ads connection id.
- **customerId** `string`: Google customer id without dashes. Required when the connection has multiple customers.
- **structuredSnippets** (required) `array`: No description
### Responses
#### 201: Assets created and attached.
**Response Body:**
- **customerId** `string`: No description
- **structuredSnippets** `array[object]`:
- **assetId** `string`: No description
- **header** `string`: No description - one of: Amenities, Brands, Courses, Degree programs, Destinations, Featured hotels, Insurance coverage, Models, Neighborhoods, Service catalog, Shows, Styles, Types
- **values** `array[string]`:
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access is required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Only supported on Google Ads.
---
## PUT /v1/ads/accounts/structured-snippets
**Update account snippets**
Edits existing Google assets in place. Send updates with assetResourceName and the fields to change. An asset is shared: changes affect every attachment using it. Omitted fields stay unchanged. The operation consumes the Google operations budget and invalidates affected cached lists.
### Request Body
- **accountId** (required) `string`: Zernio Google Ads connection id.
- **customerId** `string`: Google customer id without dashes. Required when the connection has multiple customers.
- **updates** (required) `array`: No description
### Responses
#### 200: Assets returned.
**Response Body:**
- **updated** `integer`: No description
- **customerId** `string`: No description
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access is required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Only supported on Google Ads.
---
## DELETE /v1/ads/accounts/structured-snippets
**Remove account snippet**
Removes the customer_asset attachment only. The underlying shared asset and its campaign or ad-group attachments remain.
### Request Body
- **accountId** (required) `string`: Zernio Google Ads connection id.
- **customerId** `string`: Google customer id without dashes. Required when the connection has multiple customers.
- **assetId** (required) `string`: No description
### Responses
#### 200: Assets returned.
**Response Body:**
- **removed** `boolean`: No description
- **customerId** `string`: No description
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access is required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Only supported on Google Ads.
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Create Meta ad account API Reference
Creates a durable Meta ad account in the end user's own business portfolio using
their connected Meta Ads token. Requires an active metaads accountId, Ads access,
business_management permission and business admin access. Discover portfolios with
GET /v1/ads/businesses. System-user tokens may return an empty businesses list;
supply the known business ID in that case.
The self-serve account starts without a payment method. The user must add a payment
method in Ads Manager before ads can deliver. Zernio cannot add payment methods.
Meta may require business verification and limits how many accounts a business can
create. Closing an account does not guarantee more capacity. An ad account cannot
truly be deleted, even after closing it and removing it from a business.
timezoneId is Meta's numeric ID, not an IANA timezone name. Select it from
https://developers.facebook.com/docs/marketing-api/reference/ad-account/timezone-ids/.
For example, 1 is America/Los_Angeles. Meta validates supported currencies and IDs.
endAdvertiser, mediaAgency and partner default to NONE for the self-serve flow.
The new account is added atomically to an existing scoped ad-account allowlist.
Unrestricted connections stay unrestricted. Reconnecting the same Meta identity
preserves this scope unless a caller explicitly replaces it. Discovery is nudged
immediately. Use the returned adAccountId with the existing ads endpoints.
This operation is not idempotent and Zernio never automatically retries it.
Unknown body fields are rejected. No validateOnly or dry-run option is supported.
After a timeout or a 502 with details.creationStatus=unknown, check the business
in Ads Manager before attempting another creation. A 201 with connectionUpdated=false
means the account exists but needs reconnecting with adAccountIds containing the returned ID and the previous
scoped IDs via GET /v1/connect/facebook/ads. Do not repeat the create call.
## POST /v1/ads/accounts
**Create Meta ad account**
Creates a durable Meta ad account in the end user's own business portfolio using
their connected Meta Ads token. Requires an active metaads accountId, Ads access,
business_management permission and business admin access. Discover portfolios with
GET /v1/ads/businesses. System-user tokens may return an empty businesses list;
supply the known business ID in that case.
The self-serve account starts without a payment method. The user must add a payment
method in Ads Manager before ads can deliver. Zernio cannot add payment methods.
Meta may require business verification and limits how many accounts a business can
create. Closing an account does not guarantee more capacity. An ad account cannot
truly be deleted, even after closing it and removing it from a business.
timezoneId is Meta's numeric ID, not an IANA timezone name. Select it from
https://developers.facebook.com/docs/marketing-api/reference/ad-account/timezone-ids/.
For example, 1 is America/Los_Angeles. Meta validates supported currencies and IDs.
endAdvertiser, mediaAgency and partner default to NONE for the self-serve flow.
The new account is added atomically to an existing scoped ad-account allowlist.
Unrestricted connections stay unrestricted. Reconnecting the same Meta identity
preserves this scope unless a caller explicitly replaces it. Discovery is nudged
immediately. Use the returned adAccountId with the existing ads endpoints.
This operation is not idempotent and Zernio never automatically retries it.
Unknown body fields are rejected. No validateOnly or dry-run option is supported.
After a timeout or a 502 with details.creationStatus=unknown, check the business
in Ads Manager before attempting another creation. A 201 with connectionUpdated=false
means the account exists but needs reconnecting with adAccountIds containing the returned ID and the previous
scoped IDs via GET /v1/connect/facebook/ads. Do not repeat the create call.
### Request Body
- **accountId** (required) `string`: Zernio metaads SocialAccount ID.
- **businessId** (required) `string`: Business portfolio that will own the account.
- **name** (required) `string`: Ad account name. Whitespace is trimmed.
- **currency** (required) `string`: Uppercase ISO 4217 currency supported by Meta.
- **timezoneId** (required) `integer`: Numeric Meta timezone ID from the linked timezone list. For example 1 is America/Los_Angeles.
- **endAdvertiser** `string`: End advertiser business or page ID. NONE uses the owning business.
- **mediaAgency** `string`: Media agency business or page ID. NONE for self-serve customers.
- **partner** `string`: Partner business or page ID. NONE for self-serve customers.
- **invoice** `boolean`: Request Meta invoicing. Eligibility is determined by Meta.
- **invoiceGroupId** `string`: Existing Meta invoice group ID.
- **invoicingEmails** `array`: Addresses for Meta invoices.
- **io** `boolean`: Meta insertion-order invoicing option.
- **poNumber** `string`: Purchase order number.
- **fundingId** `string`: Existing Meta funding reference. Does not add a payment method.
- **adAccountCreatedFromBmFlag** `boolean`: Meta Business Manager creation flag.
### Responses
#### 201: Ad account created. Check connectionUpdated and payment instructions.
**Response Body:**
- **adAccountId** (required) `string`: New Meta ad account ID for subsequent ads calls.
- **businessId** (required) `string`: Owning business portfolio ID.
- **connectionUpdated** (required) `boolean`: Whether the connection scope and discovery schedule were updated.
- **paymentMethodRequired** (required) `boolean`: Always true as a delivery prerequisite. This is not a live funding-source check. Confirm payment or invoicing in Ads Manager.
- **adsManagerUrl** (required) `string` (uri): Open the created account in Ads Manager.
- **nextSteps** (required) `string`: Payment setup instructions for the user.
- **warnings** (required) `array[string]`: Recovery instructions if the account could not be attached to the connection.
#### 400: Invalid input or Meta rejection. details.reason identifies creation_limit, business_verification_required, unsupported_currency, unsupported_timezone or business_unavailable when recognized.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access denied or Meta permission missing. details.reason may be business_management_required, business_admin_required or business_access_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 502: Creation outcome unknown. Check Ads Manager before repeating this non-idempotent request.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
## GET /v1/ads/accounts
**List ad accounts**
Returns the platform ad accounts available for the given account (e.g. Meta ad
accounts, TikTok advertiser IDs, Google Ads customer IDs).
Meta business-login accounts use their own system-user token. Fresh Meta discovery
includes businessId and businessName from the owning Business Manager when available;
cached entries gain these fields after the next discovery refresh.
For TikTok agencies: enumerates every advertiser under every Business Center the token
can read (paginated server-side), then chunks the lookup against TikTok's
`/advertiser/info/` endpoint (which has a per-call cap of ≤100 IDs). Solo advertisers
without a BC fall back to the OAuth-time `advertiser_ids` list. Cached for 1h on the
SocialAccount; lazy-refreshed on first call after expiry.
For Google Ads: responds `429` when Google's API quota is temporarily exhausted
(instead of an empty list). Retry after a delay.
### Parameters
- **accountId** (required) in query: Account ID
- **adAccountId** (optional) in query: Filter response to a single platform ad account ID (e.g. `act_123` for Meta, advertiser_id for TikTok). Returns at most one item.
- **limit** (optional) in query: Clamp the returned `accounts[]` length. Useful for typeahead pickers on agency tokens with hundreds of advertisers.
### Responses
#### 200: Ad accounts
**Response Body:**
- **accounts** `array[object]`:
- **id** `string`: Platform ad account ID (e.g. act_123)
- **name** `string`: No description
- **currency** `string`: No description
- **businessId** `string`: Meta only. Owning Business Manager ID when available on the grant.
- **businessName** `string`: Owning business name when supplied by the platform.
- **status** `string`: LinkedIn only. LinkedIn's own ad account status. In practice always `ACTIVE`, because the LinkedIn query filters to active accounts. Meta, Google, TikTok and Pinterest report `accountStatus` instead; X reports `approvalStatus`.
- **accountStatus**: The platform's own account status, forwarded unchanged. No JSON type is
declared because the type differs per platform: Meta sends an integer,
Google, TikTok and Pinterest send a string. Absent on LinkedIn (reports
`status`) and on X (reports `approvalStatus`).
If all you need is whether the account can run ads right now, read
`selectable` and skip this field. Read this one when you need to tell
the states apart, because they call for different responses:
- `1` ACTIVE. Running normally.
- `2` DISABLED. Disabled by Meta. Read `disableReason` to tell a policy
action apart from a billing one; they need very different follow-ups.
- `3` UNSETTLED. There is an unpaid balance, but the account still runs
ads. Not a ban.
- `7` PENDING_RISK_REVIEW. Meta is reviewing the account. Wait for the
outcome.
- `8` PENDING_SETTLEMENT. Meta blocks new ads until an outstanding
balance clears. Settle it and the account runs again.
- `9` IN_GRACE_PERIOD. Still running, on a deadline.
- `100` PENDING_CLOSURE. Scheduled to close.
- `101` CLOSED. Terminal.
- **approvalStatus** `string`: X only. X's own ad account approval status. Observed values are `ACCEPTED`, `PENDING` and `REJECTED`, but X does not publish the full vocabulary, so treat an unrecognised value as not usable. Other platforms report `accountStatus` or `status` instead.
- **disableReason** `integer`: Meta only. Meta's `disable_reason` code, forwarded unchanged. Present when `accountStatus` is `2` (DISABLED) and Meta gives a reason, which is what separates a policy action from a payment problem. Meta does not publish a stable list of values for this field, so none are enumerated here: resolve the code against Meta's own ad account reference. Absent when Meta reports no reason, or when the connected token cannot read the field.
- **timezoneName** `string`: IANA timezone of the ad account (Meta only). Drives daily-budget reset and Insights day boundaries.
- **timezoneOffsetHoursUtc** `number`: Signed UTC offset in hours, reflecting current DST (Meta only).
- **minimumDailyBudget** `number`: Meta only. Minimum daily budget for the account, in the account currency's major units. This is the impressions-billed minimum; other billing events have higher minimums. Absent when the connected token cannot read it.
- **selectable** `boolean`: Meta and X only. Whether the account can create/run ads now. Absent (treat as true) on other platforms.
- **unusableReason** `string,null`: Meta and X only. Human-readable reason when selectable is false; null when selectable.
- **cachedAt** `string,null` (date-time): Google only. When this list was fetched from Google. Null when it was never served from cache, or on other platforms.
- **stale** `boolean`: Google only. True when Google's daily API quota was exhausted and this is the last successful fetch, not a live read. Absent on other platforms.
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 422: Platform ads connection required (TikTok Ads, X Ads) or Instagram missing linked Facebook account
#### 429: The connected account's upstream platform quota is exhausted.
Reddit rate-limits per connected Reddit user (1000 requests per
10-minute window), and that budget is shared by every operation using
that account. Retry after the window resets rather than retrying
immediately; repeated calls while exhausted do not succeed and keep the
budget spent.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
## PATCH /v1/ads/accounts
**Update ad account settings**
Sets the default DSA beneficiary and payor on a Meta ad account (EU DSA, Article 26).
Set them once and every EU-targeted call to `/v1/ads/create`, `/v1/ads/boost` and
`/v1/ads/ctwa` on that ad account can omit `dsaBeneficiary`/`dsaPayor`: Meta applies
the defaults automatically.
The values are written to the ad account on Meta, the same setting Ads Manager edits.
Nothing is stored in Zernio, and defaults already set in Ads Manager work identically.
Zernio never guesses these values for you. Beneficiary and payor are legal disclosures
shown to EU users, so you must provide the entity names explicitly. Use
`GET /v1/ads/dsa-recommendations` to offer suggestions in your UI.
If `defaultDsaPayor` is omitted, the beneficiary is also set as the payor, which
covers the common case where the same entity benefits from and pays for the ads.
Read the current values back with `GET /v1/ads/dsa-defaults`.
Currently supported for Meta accounts only; other platforms return 400.
### Request Body
- **accountId** (required) `string`: Account ID (metaads, or a facebook/instagram posting account)
- **adAccountId** (required) `string`: Meta ad account ID (act_...)
- **defaultDsaBeneficiary** (required) `string`: Legal entity benefiting from ads on this ad account
- **defaultDsaPayor** `string`: Legal entity paying for ads on this ad account. Defaults to defaultDsaBeneficiary when omitted.
### Responses
#### 200: DSA defaults updated (re-read from Meta after the write)
**Response Body:**
- **adAccountId** `string`: No description
- **dsaDefaults** `object`:
- **beneficiary** `string`: No description
- **payor** `string`: No description
#### 400: Unsupported platform (non-Meta account) or invalid adAccountId
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Create a negative keyword list API Reference
Creates one Google Ads shared negative keyword list with optional initial keywords in a single atomic mutation. Daily quota is reserved for every mutate item, so large batches may return 429 before any change. This operation is not idempotent. The list is not attached to any campaign.
## GET /v1/ads/accounts/negative-keyword-lists
**List negative keyword lists**
Google Ads shared negative keyword lists (shared_set type NEGATIVE_KEYWORDS). Reads are cached for 10 minutes; quota exhaustion may return the last successful result for up to 7 days with stale=true. Customer selection is limited to this connection and its account scope.
### Parameters
- **accountId** (required) in query: No description
- **customerId** (optional) in query: No description
- **platform** (optional) in query: No description
### Responses
#### 200: Successful response.
**Response Body:**
- **lists** `array[AdNegativeKeywordList]`:
- **customerId** `string`: Resolved Google Ads customer id.
- **cachedAt** `string,null` (date-time): Last successful fetch time, or null without cache storage.
- **stale** `boolean`: True when quota exhaustion caused the last successful cached result to be served.
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access and permission to the selected account are required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: Ambiguous campaign or account selection. Use a profile-scoped key. A list still attached to a campaign may also be rejected by Google. The account may also be inactive or need reconnection (code ads_connection_required). Reconnect it and read GET /v1/accounts for its current ID before retrying.
#### 422: Google Ads connection is missing or unavailable.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Available only on Google Ads.
---
## POST /v1/ads/accounts/negative-keyword-lists
**Create a negative keyword list**
Creates one Google Ads shared negative keyword list with optional initial keywords in a single atomic mutation. Daily quota is reserved for every mutate item, so large batches may return 429 before any change. This operation is not idempotent. The list is not attached to any campaign.
### Request Body
- **accountId** (required) `string`: Zernio SocialAccount id.
- **customerId** `string`: Connected Google Ads customer id, without dashes. Required when the connection has multiple customers.
- **platform** `string`: Optional courtesy field. The resolved account or campaign determines support; other platforms return 501. - one of: facebook, instagram, tiktok, linkedin, pinterest, google, twitter, openai
- **name** (required) `string`: Nonempty list name, trimmed before use.
- **keywords** `array`: Full desired keyword set. Bare strings use broad match. Send [] to clear the list.
### Responses
#### 201: Successful response.
**Response Body:**
- **id** `string`: New shared set id.
- **resourceName** `string`: New shared set resource name.
- **created** `integer`: Number of initial keyword criteria created.
- **customerId** `string`: Resolved Google Ads customer id.
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access and permission to the selected account are required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: Ambiguous campaign or account selection. Use a profile-scoped key. A list still attached to a campaign may also be rejected by Google. The account may also be inactive or need reconnection (code ads_connection_required). Reconnect it and read GET /v1/accounts for its current ID before retrying.
#### 422: Google Ads connection is missing or unavailable.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Available only on Google Ads.
---
# Related Schema Definitions
## AdNegativeKeywordList
### Properties
- **id** `string`: Google shared set id.
- **resourceName** `string`: Google shared set resource name.
- **name** `string`: List name.
- **memberCount** `integer`: Number of keywords in the list.
- **referenceCount** `integer`: Number of resources referencing the list.
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Create custom conversion API Reference
Provision the Meta custom conversion an ads flow optimises toward, and hand back the
`customConversionId` for `promotedObject.customConversionId` on POST /v1/ads/create.
Removes the manual "create it in Ads Manager first" step.
**Reuse is ours, not Meta's.** Meta's create is not idempotent, so a retried request
would otherwise mint a duplicate carrying none of the original's optimisation history.
A non-archived conversion with the same `name` on the same `pixelId` is returned
instead of created, with `reused: true` and a 200 rather than a 201.
`rule` is forwarded verbatim in Meta's own grammar (e.g.
`{"url": {"i_contains": "thank-you"}}`); Meta validates it and rejects a malformed one
with "A conversion rule is required at creation time".
## GET /v1/accounts/{accountId}/custom-conversions
**List custom conversions**
The ad account's Meta custom conversions, including archived ones (`isArchived`).
### Parameters
- **accountId** (required) in path: Meta ads SocialAccount id.
- **adAccountId** (required) in query: Meta ad account id (act_).
### Responses
#### 200: Custom conversions
**Response Body:**
- **adAccountId** `string`: No description
- **data** `array[CustomConversion]`:
#### 400: Invalid input, or Meta rejected the query
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access required, or the token lacks the ads permissions.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
## POST /v1/accounts/{accountId}/custom-conversions
**Create custom conversion**
Provision the Meta custom conversion an ads flow optimises toward, and hand back the
`customConversionId` for `promotedObject.customConversionId` on POST /v1/ads/create.
Removes the manual "create it in Ads Manager first" step.
**Reuse is ours, not Meta's.** Meta's create is not idempotent, so a retried request
would otherwise mint a duplicate carrying none of the original's optimisation history.
A non-archived conversion with the same `name` on the same `pixelId` is returned
instead of created, with `reused: true` and a 200 rather than a 201.
`rule` is forwarded verbatim in Meta's own grammar (e.g.
`{"url": {"i_contains": "thank-you"}}`); Meta validates it and rejects a malformed one
with "A conversion rule is required at creation time".
### Parameters
- **accountId** (required) in path: Meta ads SocialAccount id.
### Request Body
- **adAccountId** (required) `string`: Platform ad account id (Meta act_, Google customer id, LinkedIn account id, ...).
- **name** (required) `string`: Also the reuse key, together with pixelId.
- **pixelId** (required) `string`: Meta pixel id (event_source_id). From GET /v1/accounts/{accountId}/tracking-tags.
- **customEventType** (required) `string`: Meta custom_event_type, e.g. LEAD, PURCHASE, OTHER.
- **rule** (required) `object`: Meta conversion rule, forwarded verbatim.
### Responses
#### 200: An existing custom conversion was reused
**Response Body:**
- **adAccountId** `string`: No description
- **customConversionId** `string`: Drops straight into promotedObject.customConversionId on POST /v1/ads/create.
- **reused** `boolean`: True when an existing conversion matched name + pixelId; the response is then a 200.
- **customConversion**: `CustomConversion` - See schema definition
#### 201: Custom conversion created
**Response Body:**
- **adAccountId** `string`: No description
- **customConversionId** `string`: Drops straight into promotedObject.customConversionId on POST /v1/ads/create.
- **reused** `boolean`: True when an existing conversion matched name + pixelId; the response is then a 200.
- **customConversion**: `CustomConversion` - See schema definition
#### 400: Invalid input, or Meta rejected the conversion (bad rule, per-account cap reached)
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access required, or the token lacks the ads permissions.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Related Schema Definitions
## CustomConversion
### Properties
- **id** `string`: No description
- **name** `string,null`: No description
- **rule** `object,null`: Meta's rule, parsed back from the string Meta stores.
- **customEventType** `string,null`: No description
- **pixelId** `string,null`: Meta's event_source_id, the pixel the rule reads from.
- **isArchived** `boolean`: No description
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
## CustomConversionResult
### Properties
- **adAccountId** `string`: No description
- **customConversionId** `string`: Drops straight into promotedObject.customConversionId on POST /v1/ads/create.
- **reused** `boolean`: True when an existing conversion matched name + pixelId; the response is then a 200.
- **customConversion**: No description
---
# Schedule a budget increase API Reference
Pre-schedule a temporary budget increase (Black Friday, a launch, a sale) instead of
editing the budget by hand on the day. Same target rule as the GET: exactly one of
`campaignId` / `adSetId`.
Two Meta constraints worth knowing before you call it. `timeStart` / `timeEnd` must
fall on a 15-minute boundary, and a campaign cannot mix `ABSOLUTE` and `MULTIPLIER`
across its schedules; the second type is rejected with "Can't mix your budget scaling
selection". Window rules (must sit inside the campaign's run dates, minimum lead time,
no overlap) are Meta's and its message is forwarded verbatim.
## GET /v1/ads/high-demand-periods
**List high-demand periods**
Scheduled budget increases (Meta's budget-scheduling API). The Graph edge lives on the
campaign and ad-set nodes only, so exactly one of `campaignId` / `adSetId` (platform
ids) is required. Rows returned verbatim (budget_value, budget_value_type, time window,
recurrence).
### Parameters
- **accountId** (required) in query: Zernio SocialAccount id (posting or ads variant) used to resolve the Meta token.
- **campaignId** (optional) in query: Platform campaign id. Exactly one of campaignId / adSetId.
- **adSetId** (optional) in query: Platform ad set id. Exactly one of campaignId / adSetId.
- **limit** (optional) in query: Rows per page
- **after** (optional) in query: Cursor from paging.after of the previous page.
### Responses
#### 200: Budget schedules (raw Meta shape)
**Response Body:**
- **objectId** `string`: The campaign / ad set id the schedules belong to.
- **data** `array[object]`:
Type: `object`
- **paging** `object`:
- **after** `string,null`: Cursor for the next page; null when exhausted.
#### 400: Invalid input, or Meta rejected the query
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 501: Only supported on Meta (facebook/instagram)
---
## POST /v1/ads/high-demand-periods
**Schedule a budget increase**
Pre-schedule a temporary budget increase (Black Friday, a launch, a sale) instead of
editing the budget by hand on the day. Same target rule as the GET: exactly one of
`campaignId` / `adSetId`.
Two Meta constraints worth knowing before you call it. `timeStart` / `timeEnd` must
fall on a 15-minute boundary, and a campaign cannot mix `ABSOLUTE` and `MULTIPLIER`
across its schedules; the second type is rejected with "Can't mix your budget scaling
selection". Window rules (must sit inside the campaign's run dates, minimum lead time,
no overlap) are Meta's and its message is forwarded verbatim.
### Request Body
- **accountId** (required) `string`: Zernio SocialAccount id used to resolve the Meta token.
- **campaignId** `string`: Platform campaign id. Exactly one of campaignId / adSetId.
- **adSetId** `string`: Platform ad set id. Exactly one of campaignId / adSetId.
- **budgetValue** (required) `number`: With ABSOLUTE, a budget in the ad account's currency in WHOLE units (50 = $50.00). With MULTIPLIER, a factor of the existing budget (2 = double it) and NOT a currency amount.
- **budgetValueType** (required) `string`: No description - one of: ABSOLUTE, MULTIPLIER
- **timeStart** (required) `integer`: Unix seconds, on a 15-minute boundary (:00, :15, :30, :45).
- **timeEnd** (required) `integer`: Unix seconds, on a 15-minute boundary and after timeStart.
- **recurrenceType** `string`: No description - one of: ONE_TIME, WEEKLY, MONTHLY
- **currency** `string`: Ad account currency, for the ABSOLUTE minor-unit conversion. Ignored for MULTIPLIER.
### Responses
#### 201: Budget schedule created
**Response Body:**
- **objectId** `string`: The campaign / ad set the schedule was attached to.
- **id** `string`: Meta budget schedule id.
#### 400: Invalid input, or Meta rejected the schedule
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 501: Only supported on Meta (facebook/instagram)
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Create a value rule set API Reference
Creates a value rule set on the ad account (Meta's `POST /act_X/value_rule_set`).
Attach the returned id to an ad set with `valueRuleSetId` on `POST /v1/ads/create` or
`PUT /v1/ads/ad-sets/{adSetId}`.
**Rule order is semantic**: rules are evaluated in array order and only the first
matching rule adjusts the bid for an overlapping audience.
`adjustValue` is an unsigned magnitude in percent; the direction lives in `adjustSign`.
`INCREASE` accepts 1-1000, `DECREASE` accepts 1-90. There is no signed field and 0 is
out of range.
`criteriaValueTypes` is positionally paired with `criteriaValues` (same length, same
order). Every type is the literal `"NONE"` except on `LOCATION`, which uses
`LOCATION_COUNTRY` / `LOCATION_REGION` / `LOCATION_CITY` / `LOCATION_COMSCORE_MARKET`
and may mix them within one criterion. Location values are Targeting-Search keys: a
two-letter country code for `LOCATION_COUNTRY`, a numeric key for the rest.
`LOCATION_DMA` was replaced by `LOCATION_COMSCORE_MARKET` on 2026-06-22 and rules using
DMAs are no longer active, so this API rejects it.
`AUDIENCE_LABEL` values (e.g. `HIGH_VALUE`) are applied to a Custom Audience in Ads
Manager. There is no API to provision them, so label strings are passed through
unvalidated and a typo produces a rule that never fires.
Ads Manager turns a rule set read-only (this API stays editable) when a rule uses more
than 2 criteria, a custom age range, or the placements `FB_MARKETPLACE`, `FB_SEARCH`,
`FB_VIDEO` or `IG_EXPLORE`.
Limits: 6 rule sets per ad account, 10 rules per set, 4 criteria per rule. The
per-account cap is enforced by Meta, not here.
## GET /v1/ads/value-rule-sets
**List value rule sets**
Lists the ad account's value rule sets (Meta's `/act_X/value_rule_set`). A value rule
set adjusts the auction bid up or down for audience segments you value differently;
attach one to an ad set with `valueRuleSetId` on `POST /v1/ads/create` or
`PUT /v1/ads/ad-sets/{adSetId}`.
Rows are returned in the same camelCase shape the `PUT` body takes, ids included, so a
set round-trips 1:1: **the update is a full replace, not a patch**, so you GET, mutate
and send the whole thing back.
Limits: 6 rule sets per ad account, 10 rules per set, 4 criteria per rule.
**Rule order is semantic.** Rules are evaluated in array order and only the FIRST
matching rule adjusts the bid for an overlapping audience. The order you send is the
order that is stored and returned.
Eligibility: value rule sets apply only to ad sets on the `LOWEST_COST_WITHOUT_CAP`
(auto-bid) or `COST_CAP` bid strategies. Meta rejects the rest server-side.
### Parameters
- **accountId** (required) in query: Zernio SocialAccount id (posting or ads variant) used to resolve the Meta token.
- **adAccountId** (required) in query: Meta ad account id (act_).
- **limit** (optional) in query: Rows per page
- **after** (optional) in query: Cursor from paging.after of the previous page. Meta does not document paging on this edge; `after` comes back null when it omits cursors.
### Responses
#### 200: Value rule sets
**Response Body:**
- **adAccountId** `string`: No description
- **data** `array[ValueRuleSet]`:
- **paging** `object`:
- **after** `string,null`: Cursor for the next page; null when exhausted or when Meta omits paging.
#### 400: Invalid input, or Meta rejected the query. Meta answers a bad rule-set id with GraphMethodException code 100 / subcode 33, which is indistinguishable between not-found, no-permission, and account-not-enabled.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 501: Only supported on Meta (facebook/instagram)
---
## POST /v1/ads/value-rule-sets
**Create a value rule set**
Creates a value rule set on the ad account (Meta's `POST /act_X/value_rule_set`).
Attach the returned id to an ad set with `valueRuleSetId` on `POST /v1/ads/create` or
`PUT /v1/ads/ad-sets/{adSetId}`.
**Rule order is semantic**: rules are evaluated in array order and only the first
matching rule adjusts the bid for an overlapping audience.
`adjustValue` is an unsigned magnitude in percent; the direction lives in `adjustSign`.
`INCREASE` accepts 1-1000, `DECREASE` accepts 1-90. There is no signed field and 0 is
out of range.
`criteriaValueTypes` is positionally paired with `criteriaValues` (same length, same
order). Every type is the literal `"NONE"` except on `LOCATION`, which uses
`LOCATION_COUNTRY` / `LOCATION_REGION` / `LOCATION_CITY` / `LOCATION_COMSCORE_MARKET`
and may mix them within one criterion. Location values are Targeting-Search keys: a
two-letter country code for `LOCATION_COUNTRY`, a numeric key for the rest.
`LOCATION_DMA` was replaced by `LOCATION_COMSCORE_MARKET` on 2026-06-22 and rules using
DMAs are no longer active, so this API rejects it.
`AUDIENCE_LABEL` values (e.g. `HIGH_VALUE`) are applied to a Custom Audience in Ads
Manager. There is no API to provision them, so label strings are passed through
unvalidated and a typo produces a rule that never fires.
Ads Manager turns a rule set read-only (this API stays editable) when a rule uses more
than 2 criteria, a custom age range, or the placements `FB_MARKETPLACE`, `FB_SEARCH`,
`FB_VIDEO` or `IG_EXPLORE`.
Limits: 6 rule sets per ad account, 10 rules per set, 4 criteria per rule. The
per-account cap is enforced by Meta, not here.
### Request Body
- **accountId** (required) `string`: Zernio SocialAccount id (posting or ads variant); its platform decides where the campaign is created.
- **adAccountId** (required) `string`: Platform ad account id (Meta act_, Google customer id, LinkedIn account id, ...).
- **name** (required) `string`: No description
- **rules** (required) `array`: Evaluated in order; the first matching rule wins.
### Responses
#### 201: Value rule set created
**Response Body:**
- **adAccountId** `string`: No description
- **valueRuleSetId** `string,null`: The new rule set id. Meta does not document the create response body, so this is null on the (unobserved) case where it omits the id.
#### 400: Invalid input, or Meta rejected the create (per-account rule-set cap, ineligible criteria, or an account that is not enabled for value rules)
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 501: Only supported on Meta (facebook/instagram)
---
# Related Schema Definitions
## ValueRuleSet
A named set of bid-adjustment rules on an ad account. Attach it to an ad set with
`valueRuleSetId`. Limits: 6 sets per ad account, 10 rules per set, 4 criteria per rule.
### Properties
- **id** (required) `string`: Platform value rule set id.
- **name** (required) `string`: No description
- **rules** (required) `array`: Evaluated in order; the first matching rule wins.
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Delete an ad comment API Reference
Delete your own TikTok ad comment or reply. TikTok must return can_delete=true for the comment. Other users' comments can be hidden instead.
Unknown identity and video item fields are resolved only when needed for this
action, then persisted for reuse. Comment-specific fields take precedence.
If TikTok no longer returns the ad needed to resolve identity, 404 ad_not_found
directs you to check deletion or archival in TikTok Ads Manager. Listing can
still succeed. Unsupported or unavailable identity returns 403 feature_not_available.
Denied access to ad details returns 403 insufficient_permissions with reconnect
guidance and the upstream platformError.
Requires Ads access. The ad is resolved within the caller's accessible profiles.
Before moderation, Zernio verifies that the comment belongs to this ad using
TikTok's ad-group comment listing. The default search window is the last 30 days.
Use since/until for older comments, with at most 30 days between the dates.
Lookups scan at most 2,000 ad-group comments; narrow the date window if exceeded.
Meta returns 501 feature_not_available with guidance to use the existing inbox
comment endpoints and the account/post IDs from GET /v1/ads/{adId}/comments.
## DELETE /v1/ads/{adId}/comments/{commentId}
**Delete an ad comment**
Delete your own TikTok ad comment or reply. TikTok must return can_delete=true for the comment. Other users' comments can be hidden instead.
Unknown identity and video item fields are resolved only when needed for this
action, then persisted for reuse. Comment-specific fields take precedence.
If TikTok no longer returns the ad needed to resolve identity, 404 ad_not_found
directs you to check deletion or archival in TikTok Ads Manager. Listing can
still succeed. Unsupported or unavailable identity returns 403 feature_not_available.
Denied access to ad details returns 403 insufficient_permissions with reconnect
guidance and the upstream platformError.
Requires Ads access. The ad is resolved within the caller's accessible profiles.
Before moderation, Zernio verifies that the comment belongs to this ad using
TikTok's ad-group comment listing. The default search window is the last 30 days.
Use since/until for older comments, with at most 30 days between the dates.
Lookups scan at most 2,000 ad-group comments; narrow the date window if exceeded.
Meta returns 501 feature_not_available with guidance to use the existing inbox
comment endpoints and the account/post IDs from GET /v1/ads/{adId}/comments.
### Parameters
- **adId** (required) in path: Internal Zernio ad ID or indexed platform ad ID.
- **commentId** (required) in path: TikTok comment ID from the ad comment listing.
- **since** (optional) in query: Start date of the comment lookup window. Defaults to 30 days before until.
- **until** (optional) in query: End date of the comment lookup window. Defaults to today in UTC.
### Responses
#### 200: Comment action completed.
**Response Body:**
- **status** (required) `string`: No description - one of: success
- **commentId** (required) `string`: ID of the created reply or moderated comment.
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access, own-comment deletion or supported identity is unavailable (feature_not_available), or TikTok denies ad-detail access (insufficient_permissions).
#### 404: Ad is inaccessible or unavailable on TikTok for identity resolution (ad_not_found), or the comment was not found on this ad in the selected date window (resource_not_found).
#### 422: TikTok Ads connection is unavailable.
#### 501: Moderation on this route supports TikTok. Use the inbox comment routes for Meta.
#### 502: TikTok rejected the request or was unavailable. Inspect platformError for its code and message.
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Delete a negative keyword list API Reference
Removes the Google shared negative keyword list. Detach it from all campaigns first; an in-use list is rejected. Only NEGATIVE_KEYWORDS shared sets are supported.
## GET /v1/ads/accounts/negative-keyword-lists/{listId}
**Get a negative keyword list**
Google Ads shared negative keyword lists (shared_set type NEGATIVE_KEYWORDS). Reads are cached for 10 minutes; quota exhaustion may return the last successful result for up to 7 days with stale=true. Customer selection is limited to this connection and its account scope. Includes the keywords and their criterion ids.
### Parameters
- **listId** (required) in path: No description
- **accountId** (required) in query: No description
- **customerId** (optional) in query: No description
- **platform** (optional) in query: No description
### Responses
#### 200: Successful response.
**Response Body:**
- **list**: No description
- **customerId** `string`: Resolved Google Ads customer id.
- **cachedAt** `string,null` (date-time): Last successful fetch time, or null without cache storage.
- **stale** `boolean`: True when quota exhaustion caused the last successful cached result to be served.
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access and permission to the selected account are required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: Ambiguous campaign or account selection. Use a profile-scoped key. A list still attached to a campaign may also be rejected by Google. The account may also be inactive or need reconnection (code ads_connection_required). Reconnect it and read GET /v1/accounts for its current ID before retrying.
#### 422: Google Ads connection is missing or unavailable.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Available only on Google Ads.
---
## PUT /v1/ads/accounts/negative-keyword-lists/{listId}
**Rename a negative keyword list**
Renames a shared negative keyword list. Keywords and campaign associations are unchanged. Use the keywords endpoint to edit the desired keyword set.
### Parameters
- **listId** (required) in path: No description
### Request Body
- **accountId** (required) `string`: Zernio SocialAccount id.
- **customerId** `string`: Connected Google Ads customer id, without dashes. Required when the connection has multiple customers.
- **platform** `string`: Optional courtesy field. The resolved account or campaign determines support; other platforms return 501. - one of: facebook, instagram, tiktok, linkedin, pinterest, google, twitter, openai
- **name** (required) `string`: Nonempty list name, trimmed before use.
### Responses
#### 200: Successful response.
**Response Body:**
- **updated** `boolean`: No description
- **customerId** `string`: Resolved Google Ads customer id.
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access and permission to the selected account are required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: Ambiguous campaign or account selection. Use a profile-scoped key. A list still attached to a campaign may also be rejected by Google. The account may also be inactive or need reconnection (code ads_connection_required). Reconnect it and read GET /v1/accounts for its current ID before retrying.
#### 422: Google Ads connection is missing or unavailable.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Available only on Google Ads.
---
## DELETE /v1/ads/accounts/negative-keyword-lists/{listId}
**Delete a negative keyword list**
Removes the Google shared negative keyword list. Detach it from all campaigns first; an in-use list is rejected. Only NEGATIVE_KEYWORDS shared sets are supported.
### Parameters
- **listId** (required) in path: No description
- **accountId** (required) in query: No description
- **customerId** (optional) in query: No description
- **platform** (optional) in query: No description
### Responses
#### 200: Successful response.
**Response Body:**
- **removed** `boolean`: No description
- **customerId** `string`: Resolved Google Ads customer id.
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access and permission to the selected account are required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: Ambiguous campaign or account selection. Use a profile-scoped key. A list still attached to a campaign may also be rejected by Google. The account may also be inactive or need reconnection (code ads_connection_required). Reconnect it and read GET /v1/accounts for its current ID before retrying.
#### 422: Google Ads connection is missing or unavailable.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Available only on Google Ads.
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Delete a value rule set API Reference
Deletes the rule set (Meta's `POST /{value-rule-set-id}/delete_rule_set`, a custom
action edge rather than an HTTP DELETE on its side). Ad sets pointing at it are not
modified here; detach them first with `valueRulesApplied: false` on
`PUT /v1/ads/ad-sets/{adSetId}`.
## GET /v1/ads/value-rule-sets/{valueRuleSetId}
**Read a value rule set**
Reads one value rule set including every nested rule id and criterion id. This is step
one of any edit: `PUT` is a full replace, so you need the ids before you can keep the
objects you are not changing.
Meta's own read returns `GENDER` values lowercase (`"male"`) while writes require
`"MALE"`. Values are passed through untouched, so never case-compare a stored rule
against a fetched one.
### Parameters
- **valueRuleSetId** (required) in path: Platform value rule set id.
- **accountId** (required) in query: Zernio SocialAccount id (posting or ads variant) used to resolve the Meta token.
### Responses
#### 200: Value rule set
**Response Body:**
- **valueRuleSet**: `ValueRuleSet` - See schema definition
#### 400: Invalid input, or Meta rejected the read. A bad id comes back as GraphMethodException code 100 / subcode 33, which cannot be told apart from a permission problem.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 501: Only supported on Meta (facebook/instagram)
---
## PUT /v1/ads/value-rule-sets/{valueRuleSetId}
**Replace a value rule set**
**THIS IS A FULL REPLACE, NOT A PATCH.** Meta's update is declarative: the body you
send becomes the rule set.
- `GET /v1/ads/value-rule-sets/{valueRuleSetId}` FIRST.
- Keep a rule or criterion by echoing its `id`.
- Create one by including the object WITHOUT an `id`.
- Delete one by OMITTING it from the array. There is no warning and no undo.
`name` and `rules` are both required for exactly this reason: a partial body would
silently destroy every rule left out.
**Rule order is semantic**: the array order you send is the evaluation order, and only
the first matching rule adjusts the bid for an overlapping audience.
Existing rule sets created elsewhere may contain `LOCATION_DMA` criteria. Those went
inert on 2026-06-22 and are rejected here; migrate them to `LOCATION_COMSCORE_MARKET`.
### Parameters
- **valueRuleSetId** (required) in path: Platform value rule set id.
### Request Body
- **accountId** (required) `string`: Zernio SocialAccount id (posting or ads variant); its platform decides where the campaign is created.
- **name** (required) `string`: Required: the update replaces the whole set.
- **rules** (required) `array`: The COMPLETE rule list. Omitting a rule deletes it on Meta.
### Responses
#### 200: Value rule set replaced
**Response Body:**
- **valueRuleSetId** `string`: No description
- **name** `string`: No description
- **rules** `array[ValueRule]`:
- **message** `string`: No description
#### 400: Invalid input, or Meta rejected the update
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 501: Only supported on Meta (facebook/instagram)
---
## DELETE /v1/ads/value-rule-sets/{valueRuleSetId}
**Delete a value rule set**
Deletes the rule set (Meta's `POST /{value-rule-set-id}/delete_rule_set`, a custom
action edge rather than an HTTP DELETE on its side). Ad sets pointing at it are not
modified here; detach them first with `valueRulesApplied: false` on
`PUT /v1/ads/ad-sets/{adSetId}`.
### Parameters
- **valueRuleSetId** (required) in path: Platform value rule set id.
- **accountId** (required) in query: Zernio SocialAccount id (posting or ads variant) used to resolve the Meta token.
### Responses
#### 200: Value rule set deleted
**Response Body:**
- **valueRuleSetId** `string`: No description
- **message** `string`: No description
#### 400: Invalid input, or Meta rejected the delete. A bad id comes back as GraphMethodException code 100 / subcode 33, which reads like a permission error rather than a 404.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 501: Only supported on Meta (facebook/instagram)
---
# Related Schema Definitions
## ValueRuleSet
A named set of bid-adjustment rules on an ad account. Attach it to an ad set with
`valueRuleSetId`. Limits: 6 sets per ad account, 10 rules per set, 4 criteria per rule.
### Properties
- **id** (required) `string`: Platform value rule set id.
- **name** (required) `string`: No description
- **rules** (required) `array`: Evaluated in order; the first matching rule wins.
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
## ValueRule
One bid-adjustment rule. Rules are evaluated in ARRAY ORDER and only the first matching
rule adjusts the bid for an overlapping audience, so the order is semantic.
### Properties
- **id** `string`: Platform rule id. Echo it on `PUT` to KEEP this rule, omit it to CREATE a new one.
A rule left out of the array entirely is DELETED.
- **name** (required) `string`: No description (max: 255)
- **adjustSign** (required) `string`: Direction of the adjustment. There is no signed value field. - one of: INCREASE, DECREASE
- **adjustValue** (required) `integer`: Unsigned percentage magnitude. `INCREASE` accepts 1-1000, `DECREASE` accepts 1-90.
0 is out of range on both.
(min: 1) (max: 1000)
- **status** `string`: Meta returns `ACTIVE` here but documents no enum for the field. Treat it as a
passthrough: echo whatever the `GET` returned, and do not synthesize values.
- **criteria** (required) `array`: All criteria on a rule must match for the rule to fire.
---
# Ad account finances API Reference
Finances of one Meta ad account: prepaid `balance`, lifetime `amountSpent`, account
`spendCap` (null = no cap) and the `fundingSource`. Money values are converted from
Meta's minor units to whole units of `currency`.
## GET /v1/ads/accounts/finance
**Ad account finances**
Finances of one Meta ad account: prepaid `balance`, lifetime `amountSpent`, account
`spendCap` (null = no cap) and the `fundingSource`. Money values are converted from
Meta's minor units to whole units of `currency`.
### Parameters
- **accountId** (required) in query: Zernio SocialAccount id (posting or ads variant) used to resolve the Meta token.
- **adAccountId** (required) in query: Meta ad account id (act_).
### Responses
#### 200: Account finances
**Response Body:**
- **adAccountId** `string`: No description
- **currency** `string`: ISO 4217 code all money values are expressed in.
- **balance** `number`: Outstanding/prepaid balance in whole currency units.
- **amountSpent** `number`: Lifetime amount spent in whole currency units.
- **spendCap** `number,null`: Account spend cap; null when none is set.
- **fundingSource** `object,null`: No description
#### 400: Invalid input, or Meta rejected the query
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 501: Only supported on Meta (facebook/instagram)
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# List comments on an ad API Reference
Returns comments on an ad's underlying creative post. Useful for moderating or analyzing
engagement on dark posts (ad creatives that never went live organically), which the
regular GET /v1/inbox/comments/{postId} endpoint cannot serve because dark posts are
not in Zernio's post database.
An ad that runs on both Facebook feed and Instagram feed has two separate underlying
posts with separate comment threads (the creative's effective_object_story_id and
effective_instagram_media_id). Use the `placement` query param to pick one; with no
param the Instagram side is returned when it exists, otherwise Facebook. The
identifiers are read from the ad record (persisted during sync) with a Marketing-API
fallback for ads that predate the field.
For Instagram-placed comments, the Instagram account that runs the ad must be connected
to Zernio, because those comments are read through that account's token. If no connected
Instagram account on the profile can read the ad's media, the call returns
ads_connection_required (the Facebook side, if any, is still readable via ?placement=facebook).
TikTok uses the connected TikTok Ads advertiser token and supports both paid video
ads and Spark Ads. `since` and `until` select a date window of at most 30 days;
the default is the last 30 days. TikTok searches by ad group, so Zernio filters
each page to this ad. A page can be empty while `pagination.hasMore` is true.
Reuse `pagination.cursor` with the same `limit`; the cursor retains the date window.
`placement` is Meta-only and returns a 400 for TikTok.
Listing needs no identity or video item ID. When the ad group is stored, each
page makes one comment-list call and no ad-detail lookup, including for external
ads that TikTok no longer returns from ad details. `meta.tiktokItemId: null`
does not prevent listing. If the ad group is missing, Zernio fetches ad details;
unavailable details return 404 ad_not_found, and no ad group returns 400 ad_not_commentable.
TikTok returns replies as separate comments with `parentId`; nested reply fetching
is not supported. `canReply` requires a first-level comment, comment-management
permission, a video item ID and a supported TT_USER or CUSTOMIZED_USER identity.
`canDelete` requires TikTok's own-comment deletion capability, a video item ID
and a supported identity. Both flags are false when identity or item is unknown.
Listing uses stored and comment-specific fields without fetching identity.
A direct reply or delete request can lazily resolve missing fields and succeed
even after a false flag. `canHide` is true because visibility changes need only
advertiser and comment IDs. `canLike` is false. Use the ad comment reply, hide
and delete operations below to moderate TikTok comments.
Other platforms return feature_not_available.
Requires the Ads add-on. Response shape matches GET /v1/inbox/comments/{postId}.
The `{adId}` path segment accepts any identifier dialect Zernio indexes for the ad:
Zernio internal `_id` (24-char hex), the numeric `platformAdId` (the value shipped in
`comment.received` webhooks as `comment.ad.id`), or the creative's
`effective_object_story_id` / `effective_instagram_media_id`. Caller doesn't need a
translation step.
## GET /v1/ads/{adId}/comments
**List comments on an ad**
Returns comments on an ad's underlying creative post. Useful for moderating or analyzing
engagement on dark posts (ad creatives that never went live organically), which the
regular GET /v1/inbox/comments/{postId} endpoint cannot serve because dark posts are
not in Zernio's post database.
An ad that runs on both Facebook feed and Instagram feed has two separate underlying
posts with separate comment threads (the creative's effective_object_story_id and
effective_instagram_media_id). Use the `placement` query param to pick one; with no
param the Instagram side is returned when it exists, otherwise Facebook. The
identifiers are read from the ad record (persisted during sync) with a Marketing-API
fallback for ads that predate the field.
For Instagram-placed comments, the Instagram account that runs the ad must be connected
to Zernio, because those comments are read through that account's token. If no connected
Instagram account on the profile can read the ad's media, the call returns
ads_connection_required (the Facebook side, if any, is still readable via ?placement=facebook).
TikTok uses the connected TikTok Ads advertiser token and supports both paid video
ads and Spark Ads. `since` and `until` select a date window of at most 30 days;
the default is the last 30 days. TikTok searches by ad group, so Zernio filters
each page to this ad. A page can be empty while `pagination.hasMore` is true.
Reuse `pagination.cursor` with the same `limit`; the cursor retains the date window.
`placement` is Meta-only and returns a 400 for TikTok.
Listing needs no identity or video item ID. When the ad group is stored, each
page makes one comment-list call and no ad-detail lookup, including for external
ads that TikTok no longer returns from ad details. `meta.tiktokItemId: null`
does not prevent listing. If the ad group is missing, Zernio fetches ad details;
unavailable details return 404 ad_not_found, and no ad group returns 400 ad_not_commentable.
TikTok returns replies as separate comments with `parentId`; nested reply fetching
is not supported. `canReply` requires a first-level comment, comment-management
permission, a video item ID and a supported TT_USER or CUSTOMIZED_USER identity.
`canDelete` requires TikTok's own-comment deletion capability, a video item ID
and a supported identity. Both flags are false when identity or item is unknown.
Listing uses stored and comment-specific fields without fetching identity.
A direct reply or delete request can lazily resolve missing fields and succeed
even after a false flag. `canHide` is true because visibility changes need only
advertiser and comment IDs. `canLike` is false. Use the ad comment reply, hide
and delete operations below to moderate TikTok comments.
Other platforms return feature_not_available.
Requires the Ads add-on. Response shape matches GET /v1/inbox/comments/{postId}.
The `{adId}` path segment accepts any identifier dialect Zernio indexes for the ad:
Zernio internal `_id` (24-char hex), the numeric `platformAdId` (the value shipped in
`comment.received` webhooks as `comment.ad.id`), or the creative's
`effective_object_story_id` / `effective_instagram_media_id`. Caller doesn't need a
translation step.
### Parameters
- **adId** (required) in path: Internal Zernio ad ID or indexed platform ad/post ID.
- **placement** (optional) in query: Which side of the ad to return comments for. Omit to default to the Instagram side when present, else Facebook. Returns ad_not_commentable if the ad has no such placement.
- **limit** (optional) in query: No description
- **since** (optional) in query: TikTok-only start date. Defaults to 30 days before until. Maximum window is 30 days.
- **until** (optional) in query: TikTok-only end date. Defaults to today in UTC.
- **cursor** (optional) in query: Pagination cursor from a previous response.
### Responses
#### 200: Comments on the ad.
**Response Body:**
- **status** (required) `string`: No description - one of: success
- **comments** (required) `array[object]`:
Type: `object`
- **pagination** (required) `object`:
- **hasMore** `boolean`: No description
- **cursor** `string`: No description
- **meta** (required) `object`:
- **platform** (required) `string`: Platform of the comments. - one of: facebook, instagram, tiktok
- **placement** `string`: The placement these comments are for, useful when you didn't pass ?placement= and want to know which one you got. - one of: facebook, instagram
- **adId** (required) `string`: Internal Zernio ad ID.
- **platformAdId** `string`: Platform ad ID.
- **effectiveStoryId** `string`: Underlying post ID the comments belong to. effective_object_story_id for the Facebook side, effective_instagram_media_id for the Instagram side.
- **tiktokItemId** `string,null`: TikTok-only video item ID from stored ad fields or returned comments. Null does not prevent listing; ad details are not fetched to populate it.
- **since** `string` (date): TikTok-only resolved start date.
- **until** `string` (date): TikTok-only resolved end date.
- **facebookAccountId** `string,null`: Facebook-only. The connected Facebook Page SocialAccount these comments were read through. Pass it as `accountId` (with `effectiveStoryId` as the postId) to /v1/inbox/comments to reply/hide/delete. Null when no connected Page was used (then moderation isn't possible).
- **instagramUserId** `string`: Instagram-only. The Instagram-scoped business ID that owns the boosted media (creative.instagram_user_id).
- **instagramPermalink** `string`: Instagram-only. Public permalink of the boosted IG post (creative.instagram_permalink_url).
- **instagramAccountId** `string`: Instagram-only. The connected Instagram SocialAccount these comments were read through. Pass it as `accountId` (with `effectiveStoryId` as the postId) to /v1/inbox/comments to reply/hide/delete.
- **accountId** (required) `string`: Account ID (ads SocialAccount).
- **lastUpdated** (required) `string` (date-time): No description
#### 400: Invalid ad ID format, or the ad's creative format does not expose a commentable
underlying post (code ad_not_commentable).
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access required (legacy plans need the Ads add-on; included by default on usage-based plans), or ad platform is not Meta or TikTok (code feature_not_available).
#### 404: Resource not found
**Response Body:**
- **error** `string`: No description (example: "Not found")
#### 422: Ads account token unavailable, or (for Instagram-placed ads) no connected
Instagram account on the profile can read the ad's media (code ads_connection_required).
---
---
# Get a negative keyword list API Reference
Google Ads shared negative keyword lists (shared_set type NEGATIVE_KEYWORDS). Reads are cached for 10 minutes; quota exhaustion may return the last successful result for up to 7 days with stale=true. Customer selection is limited to this connection and its account scope. Includes the keywords and their criterion ids.
## GET /v1/ads/accounts/negative-keyword-lists/{listId}
**Get a negative keyword list**
Google Ads shared negative keyword lists (shared_set type NEGATIVE_KEYWORDS). Reads are cached for 10 minutes; quota exhaustion may return the last successful result for up to 7 days with stale=true. Customer selection is limited to this connection and its account scope. Includes the keywords and their criterion ids.
### Parameters
- **listId** (required) in path: No description
- **accountId** (required) in query: No description
- **customerId** (optional) in query: No description
- **platform** (optional) in query: No description
### Responses
#### 200: Successful response.
**Response Body:**
- **list**: No description
- **customerId** `string`: Resolved Google Ads customer id.
- **cachedAt** `string,null` (date-time): Last successful fetch time, or null without cache storage.
- **stale** `boolean`: True when quota exhaustion caused the last successful cached result to be served.
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access and permission to the selected account are required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: Ambiguous campaign or account selection. Use a profile-scoped key. A list still attached to a campaign may also be rejected by Google. The account may also be inactive or need reconnection (code ads_connection_required). Reconnect it and read GET /v1/accounts for its current ID before retrying.
#### 422: Google Ads connection is missing or unavailable.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Available only on Google Ads.
---
## PUT /v1/ads/accounts/negative-keyword-lists/{listId}
**Rename a negative keyword list**
Renames a shared negative keyword list. Keywords and campaign associations are unchanged. Use the keywords endpoint to edit the desired keyword set.
### Parameters
- **listId** (required) in path: No description
### Request Body
- **accountId** (required) `string`: Zernio SocialAccount id.
- **customerId** `string`: Connected Google Ads customer id, without dashes. Required when the connection has multiple customers.
- **platform** `string`: Optional courtesy field. The resolved account or campaign determines support; other platforms return 501. - one of: facebook, instagram, tiktok, linkedin, pinterest, google, twitter, openai
- **name** (required) `string`: Nonempty list name, trimmed before use.
### Responses
#### 200: Successful response.
**Response Body:**
- **updated** `boolean`: No description
- **customerId** `string`: Resolved Google Ads customer id.
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access and permission to the selected account are required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: Ambiguous campaign or account selection. Use a profile-scoped key. A list still attached to a campaign may also be rejected by Google. The account may also be inactive or need reconnection (code ads_connection_required). Reconnect it and read GET /v1/accounts for its current ID before retrying.
#### 422: Google Ads connection is missing or unavailable.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Available only on Google Ads.
---
## DELETE /v1/ads/accounts/negative-keyword-lists/{listId}
**Delete a negative keyword list**
Removes the Google shared negative keyword list. Detach it from all campaigns first; an in-use list is rejected. Only NEGATIVE_KEYWORDS shared sets are supported.
### Parameters
- **listId** (required) in path: No description
- **accountId** (required) in query: No description
- **customerId** (optional) in query: No description
- **platform** (optional) in query: No description
### Responses
#### 200: Successful response.
**Response Body:**
- **removed** `boolean`: No description
- **customerId** `string`: Resolved Google Ads customer id.
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access and permission to the selected account are required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: Ambiguous campaign or account selection. Use a profile-scoped key. A list still attached to a campaign may also be rejected by Google. The account may also be inactive or need reconnection (code ads_connection_required). Reconnect it and read GET /v1/accounts for its current ID before retrying.
#### 422: Google Ads connection is missing or unavailable.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Available only on Google Ads.
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Ad account change / audit log API Reference
Account-level audit log from Meta's `/act_X/activities`: who changed what and when
(creates, edits, status flips, budget changes...) with Meta's translated event names and
the structured before/after in `extra_data`. Rows are returned verbatim. Meta has no
server-side per-object filter on this edge, so `objectId` filters the returned page
client-side (combine with paging to walk history for one campaign/ad set/ad).
## GET /v1/ads/activity
**Ad account change / audit log**
Account-level audit log from Meta's `/act_X/activities`: who changed what and when
(creates, edits, status flips, budget changes...) with Meta's translated event names and
the structured before/after in `extra_data`. Rows are returned verbatim. Meta has no
server-side per-object filter on this edge, so `objectId` filters the returned page
client-side (combine with paging to walk history for one campaign/ad set/ad).
### Parameters
- **accountId** (required) in query: Zernio SocialAccount id (posting or ads variant) used to resolve the Meta token.
- **adAccountId** (required) in query: Meta ad account id (act_).
- **since** (optional) in query: Start of range (YYYY-MM-DD).
- **until** (optional) in query: End of range (YYYY-MM-DD).
- **objectId** (optional) in query: Client-side filter to one Meta object id (campaign, ad set or ad).
- **limit** (optional) in query: Rows per page
- **after** (optional) in query: Cursor from paging.after of the previous page.
### Responses
#### 200: Activity rows (raw Meta shape)
**Response Body:**
- **adAccountId** `string`: No description
- **data** `array[object]`:
Type: `object`
- **paging** `object`:
- **after** `string,null`: Cursor for the next page; null when exhausted.
#### 400: Invalid input, or Meta rejected the query
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 501: Only supported on Meta (facebook/instagram)
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Get ad account DSA defaults API Reference
Returns the default DSA beneficiary and payor currently set on a Meta ad account,
whether they were set via `PATCH /v1/ads/accounts` or in Meta Ads Manager. Fields
are omitted when no default is configured. Meta accounts only.
## GET /v1/ads/dsa-defaults
**Get ad account DSA defaults**
Returns the default DSA beneficiary and payor currently set on a Meta ad account,
whether they were set via `PATCH /v1/ads/accounts` or in Meta Ads Manager. Fields
are omitted when no default is configured. Meta accounts only.
### Parameters
- **accountId** (required) in query: Account ID (metaads, or a facebook/instagram posting account)
- **adAccountId** (required) in query: Meta ad account ID (act_...)
### Responses
#### 200: Current DSA defaults (empty object when none are set)
**Response Body:**
- **adAccountId** `string`: No description
- **dsaDefaults** `object`:
- **beneficiary** `string`: No description
- **payor** `string`: No description
#### 400: Non-Meta adAccountId
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Get DSA recommendations API Reference
Returns Meta's suggested beneficiary/payor names for an ad account, derived by Meta
from the account's recent activity. Useful for prefilling `dsaBeneficiary`/`dsaPayor`
inputs, or the defaults sent to `PATCH /v1/ads/accounts`, in your own UI.
Meta returns a single flat list. Entries are not labeled as beneficiary or payor,
and since these are legal disclosures Zernio never applies them automatically: let
your user pick the right entity. The list may be empty for accounts with little
activity. Meta accounts only.
## GET /v1/ads/dsa-recommendations
**Get DSA recommendations**
Returns Meta's suggested beneficiary/payor names for an ad account, derived by Meta
from the account's recent activity. Useful for prefilling `dsaBeneficiary`/`dsaPayor`
inputs, or the defaults sent to `PATCH /v1/ads/accounts`, in your own UI.
Meta returns a single flat list. Entries are not labeled as beneficiary or payor,
and since these are legal disclosures Zernio never applies them automatically: let
your user pick the right entity. The list may be empty for accounts with little
activity. Meta accounts only.
### Parameters
- **accountId** (required) in query: Account ID (metaads, or a facebook/instagram posting account)
- **adAccountId** (required) in query: Meta ad account ID (act_...)
### Responses
#### 200: Suggested DSA strings (may be empty when Meta has no recommendations)
**Response Body:**
- **adAccountId** `string`: No description
- **recommendations** `array[string]`:
#### 400: Non-Meta adAccountId
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Get iOS 14 campaign limits API Reference
Reads Meta iOS 14 campaign limits for an application on an ad account. applicationId is sent as Meta app_id. This read does not establish that the application is configured for iOS promotion.
## GET /v1/ads/ios-fourteen-campaign-limits
**Get iOS 14 campaign limits**
Reads Meta iOS 14 campaign limits for an application on an ad account. applicationId is sent as Meta app_id. This read does not establish that the application is configured for iOS promotion.
### Parameters
- **accountId** (required) in query: Zernio Meta Ads or Facebook SocialAccount ID.
- **adAccountId** (required) in query: Meta ad account ID including the act_ prefix.
- **applicationId** (required) in query: Meta application ID from advertisable-applications.
### Responses
#### 200: Application campaign limits.
**Response Body:**
- **limits** (required) `object,null`: No description
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: The account or Meta asset is not accessible.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 501: Only supported on Meta Ads and Facebook accounts.
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Read a value rule set API Reference
Reads one value rule set including every nested rule id and criterion id. This is step
one of any edit: `PUT` is a full replace, so you need the ids before you can keep the
objects you are not changing.
Meta's own read returns `GENDER` values lowercase (`"male"`) while writes require
`"MALE"`. Values are passed through untouched, so never case-compare a stored rule
against a fetched one.
## GET /v1/ads/value-rule-sets/{valueRuleSetId}
**Read a value rule set**
Reads one value rule set including every nested rule id and criterion id. This is step
one of any edit: `PUT` is a full replace, so you need the ids before you can keep the
objects you are not changing.
Meta's own read returns `GENDER` values lowercase (`"male"`) while writes require
`"MALE"`. Values are passed through untouched, so never case-compare a stored rule
against a fetched one.
### Parameters
- **valueRuleSetId** (required) in path: Platform value rule set id.
- **accountId** (required) in query: Zernio SocialAccount id (posting or ads variant) used to resolve the Meta token.
### Responses
#### 200: Value rule set
**Response Body:**
- **valueRuleSet**: `ValueRuleSet` - See schema definition
#### 400: Invalid input, or Meta rejected the read. A bad id comes back as GraphMethodException code 100 / subcode 33, which cannot be told apart from a permission problem.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 501: Only supported on Meta (facebook/instagram)
---
## PUT /v1/ads/value-rule-sets/{valueRuleSetId}
**Replace a value rule set**
**THIS IS A FULL REPLACE, NOT A PATCH.** Meta's update is declarative: the body you
send becomes the rule set.
- `GET /v1/ads/value-rule-sets/{valueRuleSetId}` FIRST.
- Keep a rule or criterion by echoing its `id`.
- Create one by including the object WITHOUT an `id`.
- Delete one by OMITTING it from the array. There is no warning and no undo.
`name` and `rules` are both required for exactly this reason: a partial body would
silently destroy every rule left out.
**Rule order is semantic**: the array order you send is the evaluation order, and only
the first matching rule adjusts the bid for an overlapping audience.
Existing rule sets created elsewhere may contain `LOCATION_DMA` criteria. Those went
inert on 2026-06-22 and are rejected here; migrate them to `LOCATION_COMSCORE_MARKET`.
### Parameters
- **valueRuleSetId** (required) in path: Platform value rule set id.
### Request Body
- **accountId** (required) `string`: Zernio SocialAccount id (posting or ads variant); its platform decides where the campaign is created.
- **name** (required) `string`: Required: the update replaces the whole set.
- **rules** (required) `array`: The COMPLETE rule list. Omitting a rule deletes it on Meta.
### Responses
#### 200: Value rule set replaced
**Response Body:**
- **valueRuleSetId** `string`: No description
- **name** `string`: No description
- **rules** `array[ValueRule]`:
- **message** `string`: No description
#### 400: Invalid input, or Meta rejected the update
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 501: Only supported on Meta (facebook/instagram)
---
## DELETE /v1/ads/value-rule-sets/{valueRuleSetId}
**Delete a value rule set**
Deletes the rule set (Meta's `POST /{value-rule-set-id}/delete_rule_set`, a custom
action edge rather than an HTTP DELETE on its side). Ad sets pointing at it are not
modified here; detach them first with `valueRulesApplied: false` on
`PUT /v1/ads/ad-sets/{adSetId}`.
### Parameters
- **valueRuleSetId** (required) in path: Platform value rule set id.
- **accountId** (required) in query: Zernio SocialAccount id (posting or ads variant) used to resolve the Meta token.
### Responses
#### 200: Value rule set deleted
**Response Body:**
- **valueRuleSetId** `string`: No description
- **message** `string`: No description
#### 400: Invalid input, or Meta rejected the delete. A bad id comes back as GraphMethodException code 100 / subcode 33, which reads like a permission error rather than a 404.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 501: Only supported on Meta (facebook/instagram)
---
# Related Schema Definitions
## ValueRuleSet
A named set of bid-adjustment rules on an ad account. Attach it to an ad set with
`valueRuleSetId`. Limits: 6 sets per ad account, 10 rules per set, 4 criteria per rule.
### Properties
- **id** (required) `string`: Platform value rule set id.
- **name** (required) `string`: No description
- **rules** (required) `array`: Evaluated in order; the first matching rule wins.
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
## ValueRule
One bid-adjustment rule. Rules are evaluated in ARRAY ORDER and only the first matching
rule adjusts the bid for an overlapping audience, so the order is semantic.
### Properties
- **id** `string`: Platform rule id. Echo it on `PUT` to KEEP this rule, omit it to CREATE a new one.
A rule left out of the array entirely is DELETED.
- **name** (required) `string`: No description (max: 255)
- **adjustSign** (required) `string`: Direction of the adjustment. There is no signed value field. - one of: INCREASE, DECREASE
- **adjustValue** (required) `integer`: Unsigned percentage magnitude. `INCREASE` accepts 1-1000, `DECREASE` accepts 1-90.
0 is out of range on both.
(min: 1) (max: 1000)
- **status** `string`: Meta returns `ACTIVE` here but documents no enum for the field. Treat it as a
passthrough: echo whatever the `GET` returned, and do not synthesize values.
- **criteria** (required) `array`: All criteria on a rule must match for the rule to fire.
---
# Hide or unhide an ad comment API Reference
Hide or restore a TikTok ad comment. Send hidden=true to hide it or hidden=false to make it public again. Identity and video item ID are not required; no identity lookup is performed.
Requires Ads access. The ad is resolved within the caller's accessible profiles.
Before moderation, Zernio verifies that the comment belongs to this ad using
TikTok's ad-group comment listing. The default search window is the last 30 days.
Use since/until for older comments, with at most 30 days between the dates.
Lookups scan at most 2,000 ad-group comments; narrow the date window if exceeded.
Meta returns 501 feature_not_available with guidance to use the existing inbox
comment endpoints and the account/post IDs from GET /v1/ads/{adId}/comments.
## POST /v1/ads/{adId}/comments/{commentId}/hide
**Hide or unhide an ad comment**
Hide or restore a TikTok ad comment. Send hidden=true to hide it or hidden=false to make it public again. Identity and video item ID are not required; no identity lookup is performed.
Requires Ads access. The ad is resolved within the caller's accessible profiles.
Before moderation, Zernio verifies that the comment belongs to this ad using
TikTok's ad-group comment listing. The default search window is the last 30 days.
Use since/until for older comments, with at most 30 days between the dates.
Lookups scan at most 2,000 ad-group comments; narrow the date window if exceeded.
Meta returns 501 feature_not_available with guidance to use the existing inbox
comment endpoints and the account/post IDs from GET /v1/ads/{adId}/comments.
### Parameters
- **adId** (required) in path: Internal Zernio ad ID or indexed platform ad ID.
- **commentId** (required) in path: TikTok comment ID from the ad comment listing.
- **since** (optional) in query: Start date of the comment lookup window. Defaults to 30 days before until.
- **until** (optional) in query: End date of the comment lookup window. Defaults to today in UTC.
### Request Body
- **hidden** (required) `boolean`: True to hide the comment; false to restore it.
### Responses
#### 200: Comment action completed.
**Response Body:**
- **status** (required) `string`: No description - one of: success
- **commentId** (required) `string`: ID of the created reply or moderated comment.
- **hidden** `boolean`: The requested visibility state.
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access or the required TikTok comment capability is unavailable.
#### 404: Ad is inaccessible or the comment was not found on this ad in the selected date window.
#### 422: TikTok Ads connection is unavailable.
#### 501: Moderation on this route supports TikTok. Use the inbox comment routes for Meta.
#### 502: TikTok rejected the request or was unavailable. Inspect platformError for its code and message.
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# List account callouts API Reference
Lists directly attached Google assets. Fresh reads are cached for 10 minutes; exhausted quota may return the last successful read with stale=true. Inherited assets are not included. Preserves Google RMF C.75 account-level callouts.
## GET /v1/ads/accounts/callouts
**List account callouts**
Lists directly attached Google assets. Fresh reads are cached for 10 minutes; exhausted quota may return the last successful read with stale=true. Inherited assets are not included. Preserves Google RMF C.75 account-level callouts.
### Parameters
- **accountId** (required) in query: No description
- **customerId** (optional) in query: No description
### Responses
#### 200: Assets returned.
**Response Body:**
- **customerId** `string`: No description
- **callouts** `array[object]`:
- **assetId** `string`: No description
- **status** `string`: No description
- **text** `string`: No description
- **cachedAt** `string,null` (date-time): Time of the cached Google read. Null when no cache was used.
- **stale** `boolean`: True when exhausted quota required returning the last successful read.
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access is required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Only supported on Google Ads.
---
## POST /v1/ads/accounts/callouts
**Add account callouts**
Creates assets and customer_asset links for this Google customer. Links apply at account level.
### Request Body
- **accountId** (required) `string`: Zernio Google Ads connection id.
- **customerId** `string`: Google customer id without dashes. Required when the connection has multiple customers.
- **callouts** (required) `array`: No description
### Responses
#### 201: Assets created and attached.
**Response Body:**
- **customerId** `string`: No description
- **callouts** `array[object]`:
- **assetId** `string`: No description
- **text** `string`: No description
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access is required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Only supported on Google Ads.
---
## PUT /v1/ads/accounts/callouts
**Update account callouts**
Edits existing Google assets in place. Send updates with assetResourceName and the fields to change. An asset is shared: changes affect every attachment using it. Omitted fields stay unchanged. The operation consumes the Google operations budget and invalidates affected cached lists.
### Request Body
- **accountId** (required) `string`: Zernio Google Ads connection id.
- **customerId** `string`: Google customer id without dashes. Required when the connection has multiple customers.
- **updates** (required) `array`: No description
### Responses
#### 200: Assets returned.
**Response Body:**
- **updated** `integer`: No description
- **customerId** `string`: No description
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access is required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Only supported on Google Ads.
---
## DELETE /v1/ads/accounts/callouts
**Remove account callout**
Removes the customer_asset attachment only. The underlying shared asset and its campaign or ad-group attachments remain.
### Request Body
- **accountId** (required) `string`: Zernio Google Ads connection id.
- **customerId** `string`: Google customer id without dashes. Required when the connection has multiple customers.
- **assetId** (required) `string`: No description
### Responses
#### 200: Assets returned.
**Response Body:**
- **removed** `boolean`: No description
- **customerId** `string`: No description
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access is required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Only supported on Google Ads.
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# List account sitelinks API Reference
Lists directly attached Google assets. Fresh reads are cached for 10 minutes; exhausted quota may return the last successful read with stale=true. Inherited assets are not included.
## GET /v1/ads/accounts/sitelinks
**List account sitelinks**
Lists directly attached Google assets. Fresh reads are cached for 10 minutes; exhausted quota may return the last successful read with stale=true. Inherited assets are not included.
### Parameters
- **accountId** (required) in query: No description
- **customerId** (optional) in query: No description
### Responses
#### 200: Assets returned.
**Response Body:**
- **customerId** `string`: No description
- **sitelinks** `array[object]`:
- **assetId** `string`: No description
- **status** `string`: No description
- **assetResourceName** `string`: No description
- **customerAssetResourceName** `string`: No description
- **text** `string`: No description
- **linkUrl** `string` (uri): No description
- **description1** `string`: No description
- **description2** `string`: No description
- **cachedAt** `string,null` (date-time): Time of the cached Google read. Null when no cache was used.
- **stale** `boolean`: True when exhausted quota required returning the last successful read.
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access is required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Only supported on Google Ads.
---
## POST /v1/ads/accounts/sitelinks
**Add account sitelinks**
Creates assets and customer_asset links for this Google customer. Links apply at account level.
### Request Body
- **accountId** (required) `string`: Zernio Google Ads connection id.
- **customerId** `string`: Google customer id without dashes. Required when the connection has multiple customers.
- **sitelinks** (required) `array`: No description
### Responses
#### 201: Assets created and attached.
**Response Body:**
- **customerId** `string`: No description
- **sitelinks** `array[object]`:
- **assetId** `string`: No description
- **text** `string`: No description
- **linkUrl** `string` (uri): No description
- **description1** `string`: No description
- **description2** `string`: No description
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access is required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Only supported on Google Ads.
---
## PUT /v1/ads/accounts/sitelinks
**Update account sitelinks**
Edits existing Google assets in place. Send updates with assetResourceName and the fields to change. An asset is shared: changes affect every attachment using it. Omitted fields stay unchanged. The operation consumes the Google operations budget and invalidates affected cached lists.
### Request Body
- **accountId** (required) `string`: Zernio Google Ads connection id.
- **customerId** `string`: Google customer id without dashes. Required when the connection has multiple customers.
- **updates** (required) `array`: No description
### Responses
#### 200: Assets returned.
**Response Body:**
- **updated** `integer`: No description
- **customerId** `string`: No description
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access is required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Only supported on Google Ads.
---
## DELETE /v1/ads/accounts/sitelinks
**Remove account sitelink**
Removes the customer_asset attachment only. The underlying shared asset and its campaign or ad-group attachments remain.
### Request Body
- **accountId** (required) `string`: Zernio Google Ads connection id.
- **customerId** `string`: Google customer id without dashes. Required when the connection has multiple customers.
- **assetId** (required) `string`: No description
### Responses
#### 200: Assets returned.
**Response Body:**
- **removed** `boolean`: No description
- **customerId** `string`: No description
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access is required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Only supported on Google Ads.
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# List account snippets API Reference
Lists directly attached Google assets. Fresh reads are cached for 10 minutes; exhausted quota may return the last successful read with stale=true. Inherited assets are not included.
## GET /v1/ads/accounts/structured-snippets
**List account snippets**
Lists directly attached Google assets. Fresh reads are cached for 10 minutes; exhausted quota may return the last successful read with stale=true. Inherited assets are not included.
### Parameters
- **accountId** (required) in query: No description
- **customerId** (optional) in query: No description
### Responses
#### 200: Assets returned.
**Response Body:**
- **customerId** `string`: No description
- **structuredSnippets** `array[object]`:
- **assetId** `string`: No description
- **status** `string`: No description
- **assetResourceName** `string`: No description
- **customerAssetResourceName** `string`: No description
- **header** `string`: No description
- **values** `array[string]`:
- **cachedAt** `string,null` (date-time): Time of the cached Google read. Null when no cache was used.
- **stale** `boolean`: True when exhausted quota required returning the last successful read.
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access is required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Only supported on Google Ads.
---
## POST /v1/ads/accounts/structured-snippets
**Add account snippets**
Creates assets and customer_asset links for this Google customer. Links apply at account level.
### Request Body
- **accountId** (required) `string`: Zernio Google Ads connection id.
- **customerId** `string`: Google customer id without dashes. Required when the connection has multiple customers.
- **structuredSnippets** (required) `array`: No description
### Responses
#### 201: Assets created and attached.
**Response Body:**
- **customerId** `string`: No description
- **structuredSnippets** `array[object]`:
- **assetId** `string`: No description
- **header** `string`: No description - one of: Amenities, Brands, Courses, Degree programs, Destinations, Featured hotels, Insurance coverage, Models, Neighborhoods, Service catalog, Shows, Styles, Types
- **values** `array[string]`:
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access is required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Only supported on Google Ads.
---
## PUT /v1/ads/accounts/structured-snippets
**Update account snippets**
Edits existing Google assets in place. Send updates with assetResourceName and the fields to change. An asset is shared: changes affect every attachment using it. Omitted fields stay unchanged. The operation consumes the Google operations budget and invalidates affected cached lists.
### Request Body
- **accountId** (required) `string`: Zernio Google Ads connection id.
- **customerId** `string`: Google customer id without dashes. Required when the connection has multiple customers.
- **updates** (required) `array`: No description
### Responses
#### 200: Assets returned.
**Response Body:**
- **updated** `integer`: No description
- **customerId** `string`: No description
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access is required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Only supported on Google Ads.
---
## DELETE /v1/ads/accounts/structured-snippets
**Remove account snippet**
Removes the customer_asset attachment only. The underlying shared asset and its campaign or ad-group attachments remain.
### Request Body
- **accountId** (required) `string`: Zernio Google Ads connection id.
- **customerId** `string`: Google customer id without dashes. Required when the connection has multiple customers.
- **assetId** (required) `string`: No description
### Responses
#### 200: Assets returned.
**Response Body:**
- **removed** `boolean`: No description
- **customerId** `string`: No description
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access is required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Only supported on Google Ads.
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# List ad accounts API Reference
Returns the platform ad accounts available for the given account (e.g. Meta ad
accounts, TikTok advertiser IDs, Google Ads customer IDs).
Meta business-login accounts use their own system-user token. Fresh Meta discovery
includes businessId and businessName from the owning Business Manager when available;
cached entries gain these fields after the next discovery refresh.
For TikTok agencies: enumerates every advertiser under every Business Center the token
can read (paginated server-side), then chunks the lookup against TikTok's
`/advertiser/info/` endpoint (which has a per-call cap of ≤100 IDs). Solo advertisers
without a BC fall back to the OAuth-time `advertiser_ids` list. Cached for 1h on the
SocialAccount; lazy-refreshed on first call after expiry.
For Google Ads: responds `429` when Google's API quota is temporarily exhausted
(instead of an empty list). Retry after a delay.
## POST /v1/ads/accounts
**Create Meta ad account**
Creates a durable Meta ad account in the end user's own business portfolio using
their connected Meta Ads token. Requires an active metaads accountId, Ads access,
business_management permission and business admin access. Discover portfolios with
GET /v1/ads/businesses. System-user tokens may return an empty businesses list;
supply the known business ID in that case.
The self-serve account starts without a payment method. The user must add a payment
method in Ads Manager before ads can deliver. Zernio cannot add payment methods.
Meta may require business verification and limits how many accounts a business can
create. Closing an account does not guarantee more capacity. An ad account cannot
truly be deleted, even after closing it and removing it from a business.
timezoneId is Meta's numeric ID, not an IANA timezone name. Select it from
https://developers.facebook.com/docs/marketing-api/reference/ad-account/timezone-ids/.
For example, 1 is America/Los_Angeles. Meta validates supported currencies and IDs.
endAdvertiser, mediaAgency and partner default to NONE for the self-serve flow.
The new account is added atomically to an existing scoped ad-account allowlist.
Unrestricted connections stay unrestricted. Reconnecting the same Meta identity
preserves this scope unless a caller explicitly replaces it. Discovery is nudged
immediately. Use the returned adAccountId with the existing ads endpoints.
This operation is not idempotent and Zernio never automatically retries it.
Unknown body fields are rejected. No validateOnly or dry-run option is supported.
After a timeout or a 502 with details.creationStatus=unknown, check the business
in Ads Manager before attempting another creation. A 201 with connectionUpdated=false
means the account exists but needs reconnecting with adAccountIds containing the returned ID and the previous
scoped IDs via GET /v1/connect/facebook/ads. Do not repeat the create call.
### Request Body
- **accountId** (required) `string`: Zernio metaads SocialAccount ID.
- **businessId** (required) `string`: Business portfolio that will own the account.
- **name** (required) `string`: Ad account name. Whitespace is trimmed.
- **currency** (required) `string`: Uppercase ISO 4217 currency supported by Meta.
- **timezoneId** (required) `integer`: Numeric Meta timezone ID from the linked timezone list. For example 1 is America/Los_Angeles.
- **endAdvertiser** `string`: End advertiser business or page ID. NONE uses the owning business.
- **mediaAgency** `string`: Media agency business or page ID. NONE for self-serve customers.
- **partner** `string`: Partner business or page ID. NONE for self-serve customers.
- **invoice** `boolean`: Request Meta invoicing. Eligibility is determined by Meta.
- **invoiceGroupId** `string`: Existing Meta invoice group ID.
- **invoicingEmails** `array`: Addresses for Meta invoices.
- **io** `boolean`: Meta insertion-order invoicing option.
- **poNumber** `string`: Purchase order number.
- **fundingId** `string`: Existing Meta funding reference. Does not add a payment method.
- **adAccountCreatedFromBmFlag** `boolean`: Meta Business Manager creation flag.
### Responses
#### 201: Ad account created. Check connectionUpdated and payment instructions.
**Response Body:**
- **adAccountId** (required) `string`: New Meta ad account ID for subsequent ads calls.
- **businessId** (required) `string`: Owning business portfolio ID.
- **connectionUpdated** (required) `boolean`: Whether the connection scope and discovery schedule were updated.
- **paymentMethodRequired** (required) `boolean`: Always true as a delivery prerequisite. This is not a live funding-source check. Confirm payment or invoicing in Ads Manager.
- **adsManagerUrl** (required) `string` (uri): Open the created account in Ads Manager.
- **nextSteps** (required) `string`: Payment setup instructions for the user.
- **warnings** (required) `array[string]`: Recovery instructions if the account could not be attached to the connection.
#### 400: Invalid input or Meta rejection. details.reason identifies creation_limit, business_verification_required, unsupported_currency, unsupported_timezone or business_unavailable when recognized.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access denied or Meta permission missing. details.reason may be business_management_required, business_admin_required or business_access_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 502: Creation outcome unknown. Check Ads Manager before repeating this non-idempotent request.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
## GET /v1/ads/accounts
**List ad accounts**
Returns the platform ad accounts available for the given account (e.g. Meta ad
accounts, TikTok advertiser IDs, Google Ads customer IDs).
Meta business-login accounts use their own system-user token. Fresh Meta discovery
includes businessId and businessName from the owning Business Manager when available;
cached entries gain these fields after the next discovery refresh.
For TikTok agencies: enumerates every advertiser under every Business Center the token
can read (paginated server-side), then chunks the lookup against TikTok's
`/advertiser/info/` endpoint (which has a per-call cap of ≤100 IDs). Solo advertisers
without a BC fall back to the OAuth-time `advertiser_ids` list. Cached for 1h on the
SocialAccount; lazy-refreshed on first call after expiry.
For Google Ads: responds `429` when Google's API quota is temporarily exhausted
(instead of an empty list). Retry after a delay.
### Parameters
- **accountId** (required) in query: Account ID
- **adAccountId** (optional) in query: Filter response to a single platform ad account ID (e.g. `act_123` for Meta, advertiser_id for TikTok). Returns at most one item.
- **limit** (optional) in query: Clamp the returned `accounts[]` length. Useful for typeahead pickers on agency tokens with hundreds of advertisers.
### Responses
#### 200: Ad accounts
**Response Body:**
- **accounts** `array[object]`:
- **id** `string`: Platform ad account ID (e.g. act_123)
- **name** `string`: No description
- **currency** `string`: No description
- **businessId** `string`: Meta only. Owning Business Manager ID when available on the grant.
- **businessName** `string`: Owning business name when supplied by the platform.
- **status** `string`: LinkedIn only. LinkedIn's own ad account status. In practice always `ACTIVE`, because the LinkedIn query filters to active accounts. Meta, Google, TikTok and Pinterest report `accountStatus` instead; X reports `approvalStatus`.
- **accountStatus**: The platform's own account status, forwarded unchanged. No JSON type is
declared because the type differs per platform: Meta sends an integer,
Google, TikTok and Pinterest send a string. Absent on LinkedIn (reports
`status`) and on X (reports `approvalStatus`).
If all you need is whether the account can run ads right now, read
`selectable` and skip this field. Read this one when you need to tell
the states apart, because they call for different responses:
- `1` ACTIVE. Running normally.
- `2` DISABLED. Disabled by Meta. Read `disableReason` to tell a policy
action apart from a billing one; they need very different follow-ups.
- `3` UNSETTLED. There is an unpaid balance, but the account still runs
ads. Not a ban.
- `7` PENDING_RISK_REVIEW. Meta is reviewing the account. Wait for the
outcome.
- `8` PENDING_SETTLEMENT. Meta blocks new ads until an outstanding
balance clears. Settle it and the account runs again.
- `9` IN_GRACE_PERIOD. Still running, on a deadline.
- `100` PENDING_CLOSURE. Scheduled to close.
- `101` CLOSED. Terminal.
- **approvalStatus** `string`: X only. X's own ad account approval status. Observed values are `ACCEPTED`, `PENDING` and `REJECTED`, but X does not publish the full vocabulary, so treat an unrecognised value as not usable. Other platforms report `accountStatus` or `status` instead.
- **disableReason** `integer`: Meta only. Meta's `disable_reason` code, forwarded unchanged. Present when `accountStatus` is `2` (DISABLED) and Meta gives a reason, which is what separates a policy action from a payment problem. Meta does not publish a stable list of values for this field, so none are enumerated here: resolve the code against Meta's own ad account reference. Absent when Meta reports no reason, or when the connected token cannot read the field.
- **timezoneName** `string`: IANA timezone of the ad account (Meta only). Drives daily-budget reset and Insights day boundaries.
- **timezoneOffsetHoursUtc** `number`: Signed UTC offset in hours, reflecting current DST (Meta only).
- **minimumDailyBudget** `number`: Meta only. Minimum daily budget for the account, in the account currency's major units. This is the impressions-billed minimum; other billing events have higher minimums. Absent when the connected token cannot read it.
- **selectable** `boolean`: Meta and X only. Whether the account can create/run ads now. Absent (treat as true) on other platforms.
- **unusableReason** `string,null`: Meta and X only. Human-readable reason when selectable is false; null when selectable.
- **cachedAt** `string,null` (date-time): Google only. When this list was fetched from Google. Null when it was never served from cache, or on other platforms.
- **stale** `boolean`: Google only. True when Google's daily API quota was exhausted and this is the last successful fetch, not a live read. Absent on other platforms.
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 422: Platform ads connection required (TikTok Ads, X Ads) or Instagram missing linked Facebook account
#### 429: The connected account's upstream platform quota is exhausted.
Reddit rate-limits per connected Reddit user (1000 requests per
10-minute window), and that budget is shared by every operation using
that account. Retry after the window resets rather than retrying
immediately; repeated calls while exhausted do not succeed and keep the
budget spent.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
## PATCH /v1/ads/accounts
**Update ad account settings**
Sets the default DSA beneficiary and payor on a Meta ad account (EU DSA, Article 26).
Set them once and every EU-targeted call to `/v1/ads/create`, `/v1/ads/boost` and
`/v1/ads/ctwa` on that ad account can omit `dsaBeneficiary`/`dsaPayor`: Meta applies
the defaults automatically.
The values are written to the ad account on Meta, the same setting Ads Manager edits.
Nothing is stored in Zernio, and defaults already set in Ads Manager work identically.
Zernio never guesses these values for you. Beneficiary and payor are legal disclosures
shown to EU users, so you must provide the entity names explicitly. Use
`GET /v1/ads/dsa-recommendations` to offer suggestions in your UI.
If `defaultDsaPayor` is omitted, the beneficiary is also set as the payor, which
covers the common case where the same entity benefits from and pays for the ads.
Read the current values back with `GET /v1/ads/dsa-defaults`.
Currently supported for Meta accounts only; other platforms return 400.
### Request Body
- **accountId** (required) `string`: Account ID (metaads, or a facebook/instagram posting account)
- **adAccountId** (required) `string`: Meta ad account ID (act_...)
- **defaultDsaBeneficiary** (required) `string`: Legal entity benefiting from ads on this ad account
- **defaultDsaPayor** `string`: Legal entity paying for ads on this ad account. Defaults to defaultDsaBeneficiary when omitted.
### Responses
#### 200: DSA defaults updated (re-read from Meta after the write)
**Response Body:**
- **adAccountId** `string`: No description
- **dsaDefaults** `object`:
- **beneficiary** `string`: No description
- **payor** `string`: No description
#### 400: Unsupported platform (non-Meta account) or invalid adAccountId
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Ad labels API Reference
Lists the ad account's organizational labels (Meta's `/act_X/adlabels`), rows returned
verbatim (id, name, created/updated time).
## GET /v1/ads/labels
**Ad labels**
Lists the ad account's organizational labels (Meta's `/act_X/adlabels`), rows returned
verbatim (id, name, created/updated time).
### Parameters
- **accountId** (required) in query: Zernio SocialAccount id (posting or ads variant) used to resolve the Meta token.
- **adAccountId** (required) in query: Meta ad account id (act_).
- **limit** (optional) in query: Rows per page
- **after** (optional) in query: Cursor from paging.after of the previous page.
### Responses
#### 200: Ad labels (raw Meta shape)
**Response Body:**
- **adAccountId** `string`: No description
- **data** `array[object]`:
Type: `object`
- **paging** `object`:
- **after** `string,null`: Cursor for the next page; null when exhausted.
#### 400: Invalid input, or Meta rejected the query
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 501: Only supported on Meta (facebook/instagram)
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# List negative keyword lists API Reference
Google Ads shared negative keyword lists (shared_set type NEGATIVE_KEYWORDS). Reads are cached for 10 minutes; quota exhaustion may return the last successful result for up to 7 days with stale=true. Customer selection is limited to this connection and its account scope.
## GET /v1/ads/accounts/negative-keyword-lists
**List negative keyword lists**
Google Ads shared negative keyword lists (shared_set type NEGATIVE_KEYWORDS). Reads are cached for 10 minutes; quota exhaustion may return the last successful result for up to 7 days with stale=true. Customer selection is limited to this connection and its account scope.
### Parameters
- **accountId** (required) in query: No description
- **customerId** (optional) in query: No description
- **platform** (optional) in query: No description
### Responses
#### 200: Successful response.
**Response Body:**
- **lists** `array[AdNegativeKeywordList]`:
- **customerId** `string`: Resolved Google Ads customer id.
- **cachedAt** `string,null` (date-time): Last successful fetch time, or null without cache storage.
- **stale** `boolean`: True when quota exhaustion caused the last successful cached result to be served.
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access and permission to the selected account are required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: Ambiguous campaign or account selection. Use a profile-scoped key. A list still attached to a campaign may also be rejected by Google. The account may also be inactive or need reconnection (code ads_connection_required). Reconnect it and read GET /v1/accounts for its current ID before retrying.
#### 422: Google Ads connection is missing or unavailable.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Available only on Google Ads.
---
## POST /v1/ads/accounts/negative-keyword-lists
**Create a negative keyword list**
Creates one Google Ads shared negative keyword list with optional initial keywords in a single atomic mutation. Daily quota is reserved for every mutate item, so large batches may return 429 before any change. This operation is not idempotent. The list is not attached to any campaign.
### Request Body
- **accountId** (required) `string`: Zernio SocialAccount id.
- **customerId** `string`: Connected Google Ads customer id, without dashes. Required when the connection has multiple customers.
- **platform** `string`: Optional courtesy field. The resolved account or campaign determines support; other platforms return 501. - one of: facebook, instagram, tiktok, linkedin, pinterest, google, twitter, openai
- **name** (required) `string`: Nonempty list name, trimmed before use.
- **keywords** `array`: Full desired keyword set. Bare strings use broad match. Send [] to clear the list.
### Responses
#### 201: Successful response.
**Response Body:**
- **id** `string`: New shared set id.
- **resourceName** `string`: New shared set resource name.
- **created** `integer`: Number of initial keyword criteria created.
- **customerId** `string`: Resolved Google Ads customer id.
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access and permission to the selected account are required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: Ambiguous campaign or account selection. Use a profile-scoped key. A list still attached to a campaign may also be rejected by Google. The account may also be inactive or need reconnection (code ads_connection_required). Reconnect it and read GET /v1/accounts for its current ID before retrying.
#### 422: Google Ads connection is missing or unavailable.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Available only on Google Ads.
---
# Related Schema Definitions
## AdNegativeKeywordList
### Properties
- **id** `string`: Google shared set id.
- **resourceName** `string`: Google shared set resource name.
- **name** `string`: List name.
- **memberCount** `integer`: Number of keywords in the list.
- **referenceCount** `integer`: Number of resources referencing the list.
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# A/B tests and lift studies API Reference
Lists the ad account's A/B tests and lift studies (Meta's `/act_X/ad_studies`), rows
returned verbatim. The default projection covers id, name, type, timing and cells with
split percentages; `fields` is a raw-passthrough override.
## GET /v1/ads/studies
**A/B tests and lift studies**
Lists the ad account's A/B tests and lift studies (Meta's `/act_X/ad_studies`), rows
returned verbatim. The default projection covers id, name, type, timing and cells with
split percentages; `fields` is a raw-passthrough override.
### Parameters
- **accountId** (required) in query: Zernio SocialAccount id (posting or ads variant) used to resolve the Meta token.
- **adAccountId** (required) in query: Meta ad account id (act_).
- **fields** (optional) in query: Comma-separated Graph field override. Supports nested {} projections and Graph field modifiers, so a nested edge can be paged explicitly: without a .limit() modifier the expansion runs at the Meta default page size and the tail is dropped silently.
- **limit** (optional) in query: Rows per page
- **after** (optional) in query: Cursor from paging.after of the previous page.
### Responses
#### 200: Ad studies (raw Meta shape)
**Response Body:**
- **adAccountId** `string`: No description
- **data** `array[object]`:
Type: `object`
- **paging** `object`:
- **after** `string,null`: Cursor for the next page; null when exhausted.
#### 400: Invalid input, or Meta rejected the query
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 501: Only supported on Meta (facebook/instagram)
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# List TikTok Business Centers API Reference
Returns the TikTok Business Centers (BCs) the connected `tiktokads` account can read.
Each BC reports its advertiser count so callers can build agency-style pickers
without re-walking `/v1/ads/accounts` per BC.
TikTok-only. Solo advertisers (non-agency tokens) return an empty array.
## GET /v1/ads/business-centers
**List TikTok Business Centers**
Returns the TikTok Business Centers (BCs) the connected `tiktokads` account can read.
Each BC reports its advertiser count so callers can build agency-style pickers
without re-walking `/v1/ads/accounts` per BC.
TikTok-only. Solo advertisers (non-agency tokens) return an empty array.
### Parameters
- **accountId** (required) in query: ID of the `tiktokads` (or parent `tiktok` posting) SocialAccount
### Responses
#### 200: Business centers
**Response Body:**
- **businessCenters** `array[BusinessCenter]`:
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 422: TikTok Ads not connected
---
# Related Schema Definitions
## BusinessCenter
TikTok Business Center entity. Returned by `GET /v1/ads/business-centers`. BCs are
TikTok's agency container: one BC owns N advertisers (ad accounts). Most solo
advertisers don't have one; the agency token uses BCs to roll up multi-client access.
### Properties
- **bcId** `string`: Business Center ID
- **name** `string`: Display name set by the BC owner
- **advertiserCount** `integer,null`: Number of advertisers reachable under this BC for the calling token.
`null` when the BC asset walk returned empty or failed (typical for
agency apps without full BC asset read scope), distinct from `0`,
which would imply the BC genuinely has no advertisers.
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# List Instagram ad identities API Reference
Discovers identities through connected_instagram_accounts, Page linkage and Page-backed identities, with a best-effort business fallback. Business permission errors do not fail discovery. The resolved object uses the same profile-scoped resolver as ad creation; null means no identity was resolved. Format-specific observed-actor fallbacks at creative creation are not predicted.
## GET /v1/ads/instagram-accounts
**List Instagram ad identities**
Discovers identities through connected_instagram_accounts, Page linkage and Page-backed identities, with a best-effort business fallback. Business permission errors do not fail discovery. The resolved object uses the same profile-scoped resolver as ad creation; null means no identity was resolved. Format-specific observed-actor fallbacks at creative creation are not predicted.
### Parameters
- **accountId** (required) in query: Zernio Meta Ads or Facebook SocialAccount ID.
- **adAccountId** (required) in query: Meta ad account ID including the act_ prefix.
### Responses
#### 200: Instagram identities and Page linkage.
**Response Body:**
- **accounts** (required) `array[items]`:
- **pages** (required) `array[object]`:
- **pageId** (required) `string`: Facebook Page ID.
- **name** (required) `string`: Facebook Page name.
- **instagramBusinessAccount**: `MetaInstagramIdentityRef` - See schema definition
- **connectedInstagramAccount**: `MetaInstagramIdentityRef` - See schema definition
- **resolved** (required) `object`:
- **pageId** (required) `string,null`: Page selected by the shared ad-creation resolver.
- **igUserId** (required) `string,null`: Instagram identity selected by the shared ad-creation resolver.
- **source** (required) `string,null`: Discovery source of the resolved identity; null when absent from discovery. - one of: ad_account, page_backed, business,
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: The account or Meta asset is not accessible.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 501: Only supported on Meta Ads and Facebook accounts.
---
# Related Schema Definitions
## MetaInstagramIdentityRef
### Properties
- **igUserId** (required) `string`: Instagram identity ID.
- **username** (required) `string`: Instagram username; empty when Meta does not expose it.
- **profilePictureUrl** `string`: Profile picture URL when available.
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# List advertisable apps API Reference
Lists applications available to a Meta ad account, their supported platforms and unmodified object store URLs. A listed app still needs a configured mobile platform and store URL to run install promotion.
## GET /v1/ads/advertisable-applications
**List advertisable apps**
Lists applications available to a Meta ad account, their supported platforms and unmodified object store URLs. A listed app still needs a configured mobile platform and store URL to run install promotion.
### Parameters
- **accountId** (required) in query: Zernio Meta Ads or Facebook SocialAccount ID.
- **adAccountId** (required) in query: Meta ad account ID including the act_ prefix.
### Responses
#### 200: Applications available for promotion.
**Response Body:**
- **applications** (required) `array[object]`:
- **id** (required) `string`: Meta application ID.
- **name** (required) `string`: Application name.
- **supportedPlatforms** (required) `array[string]`: Platform identifiers reported by Meta.
- **storeUrls** (required) `object`: Platform-keyed store URLs returned unchanged by Meta.
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: The account or Meta asset is not accessible.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 501: Only supported on Meta Ads and Facebook accounts.
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# List custom conversions API Reference
The ad account's Meta custom conversions, including archived ones (`isArchived`).
## GET /v1/accounts/{accountId}/custom-conversions
**List custom conversions**
The ad account's Meta custom conversions, including archived ones (`isArchived`).
### Parameters
- **accountId** (required) in path: Meta ads SocialAccount id.
- **adAccountId** (required) in query: Meta ad account id (act_).
### Responses
#### 200: Custom conversions
**Response Body:**
- **adAccountId** `string`: No description
- **data** `array[CustomConversion]`:
#### 400: Invalid input, or Meta rejected the query
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access required, or the token lacks the ads permissions.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
## POST /v1/accounts/{accountId}/custom-conversions
**Create custom conversion**
Provision the Meta custom conversion an ads flow optimises toward, and hand back the
`customConversionId` for `promotedObject.customConversionId` on POST /v1/ads/create.
Removes the manual "create it in Ads Manager first" step.
**Reuse is ours, not Meta's.** Meta's create is not idempotent, so a retried request
would otherwise mint a duplicate carrying none of the original's optimisation history.
A non-archived conversion with the same `name` on the same `pixelId` is returned
instead of created, with `reused: true` and a 200 rather than a 201.
`rule` is forwarded verbatim in Meta's own grammar (e.g.
`{"url": {"i_contains": "thank-you"}}`); Meta validates it and rejects a malformed one
with "A conversion rule is required at creation time".
### Parameters
- **accountId** (required) in path: Meta ads SocialAccount id.
### Request Body
- **adAccountId** (required) `string`: Platform ad account id (Meta act_, Google customer id, LinkedIn account id, ...).
- **name** (required) `string`: Also the reuse key, together with pixelId.
- **pixelId** (required) `string`: Meta pixel id (event_source_id). From GET /v1/accounts/{accountId}/tracking-tags.
- **customEventType** (required) `string`: Meta custom_event_type, e.g. LEAD, PURCHASE, OTHER.
- **rule** (required) `object`: Meta conversion rule, forwarded verbatim.
### Responses
#### 200: An existing custom conversion was reused
**Response Body:**
- **adAccountId** `string`: No description
- **customConversionId** `string`: Drops straight into promotedObject.customConversionId on POST /v1/ads/create.
- **reused** `boolean`: True when an existing conversion matched name + pixelId; the response is then a 200.
- **customConversion**: `CustomConversion` - See schema definition
#### 201: Custom conversion created
**Response Body:**
- **adAccountId** `string`: No description
- **customConversionId** `string`: Drops straight into promotedObject.customConversionId on POST /v1/ads/create.
- **reused** `boolean`: True when an existing conversion matched name + pixelId; the response is then a 200.
- **customConversion**: `CustomConversion` - See schema definition
#### 400: Invalid input, or Meta rejected the conversion (bad rule, per-account cap reached)
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access required, or the token lacks the ads permissions.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Related Schema Definitions
## CustomConversion
### Properties
- **id** `string`: No description
- **name** `string,null`: No description
- **rule** `object,null`: Meta's rule, parsed back from the string Meta stores.
- **customEventType** `string,null`: No description
- **pixelId** `string,null`: Meta's event_source_id, the pixel the rule reads from.
- **isArchived** `boolean`: No description
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
## CustomConversionResult
### Properties
- **adAccountId** `string`: No description
- **customConversionId** `string`: Drops straight into promotedObject.customConversionId on POST /v1/ads/create.
- **reused** `boolean`: True when an existing conversion matched name + pixelId; the response is then a 200.
- **customConversion**: No description
---
# List high-demand periods API Reference
Scheduled budget increases (Meta's budget-scheduling API). The Graph edge lives on the
campaign and ad-set nodes only, so exactly one of `campaignId` / `adSetId` (platform
ids) is required. Rows returned verbatim (budget_value, budget_value_type, time window,
recurrence).
## GET /v1/ads/high-demand-periods
**List high-demand periods**
Scheduled budget increases (Meta's budget-scheduling API). The Graph edge lives on the
campaign and ad-set nodes only, so exactly one of `campaignId` / `adSetId` (platform
ids) is required. Rows returned verbatim (budget_value, budget_value_type, time window,
recurrence).
### Parameters
- **accountId** (required) in query: Zernio SocialAccount id (posting or ads variant) used to resolve the Meta token.
- **campaignId** (optional) in query: Platform campaign id. Exactly one of campaignId / adSetId.
- **adSetId** (optional) in query: Platform ad set id. Exactly one of campaignId / adSetId.
- **limit** (optional) in query: Rows per page
- **after** (optional) in query: Cursor from paging.after of the previous page.
### Responses
#### 200: Budget schedules (raw Meta shape)
**Response Body:**
- **objectId** `string`: The campaign / ad set id the schedules belong to.
- **data** `array[object]`:
Type: `object`
- **paging** `object`:
- **after** `string,null`: Cursor for the next page; null when exhausted.
#### 400: Invalid input, or Meta rejected the query
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 501: Only supported on Meta (facebook/instagram)
---
## POST /v1/ads/high-demand-periods
**Schedule a budget increase**
Pre-schedule a temporary budget increase (Black Friday, a launch, a sale) instead of
editing the budget by hand on the day. Same target rule as the GET: exactly one of
`campaignId` / `adSetId`.
Two Meta constraints worth knowing before you call it. `timeStart` / `timeEnd` must
fall on a 15-minute boundary, and a campaign cannot mix `ABSOLUTE` and `MULTIPLIER`
across its schedules; the second type is rejected with "Can't mix your budget scaling
selection". Window rules (must sit inside the campaign's run dates, minimum lead time,
no overlap) are Meta's and its message is forwarded verbatim.
### Request Body
- **accountId** (required) `string`: Zernio SocialAccount id used to resolve the Meta token.
- **campaignId** `string`: Platform campaign id. Exactly one of campaignId / adSetId.
- **adSetId** `string`: Platform ad set id. Exactly one of campaignId / adSetId.
- **budgetValue** (required) `number`: With ABSOLUTE, a budget in the ad account's currency in WHOLE units (50 = $50.00). With MULTIPLIER, a factor of the existing budget (2 = double it) and NOT a currency amount.
- **budgetValueType** (required) `string`: No description - one of: ABSOLUTE, MULTIPLIER
- **timeStart** (required) `integer`: Unix seconds, on a 15-minute boundary (:00, :15, :30, :45).
- **timeEnd** (required) `integer`: Unix seconds, on a 15-minute boundary and after timeStart.
- **recurrenceType** `string`: No description - one of: ONE_TIME, WEEKLY, MONTHLY
- **currency** `string`: Ad account currency, for the ABSOLUTE minor-unit conversion. Ignored for MULTIPLIER.
### Responses
#### 201: Budget schedule created
**Response Body:**
- **objectId** `string`: The campaign / ad set the schedule was attached to.
- **id** `string`: Meta budget schedule id.
#### 400: Invalid input, or Meta rejected the schedule
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 501: Only supported on Meta (facebook/instagram)
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Businesses list API Reference
Business Manager portfolios the connected Meta user belongs to (Meta's `/me/businesses`),
rows returned verbatim (id, name, verification_status, created_time). Token-scoped, so no
`adAccountId` is needed. For TikTok Business Centers use
`GET /v1/ads/business-centers`.
## GET /v1/ads/businesses
**Businesses list**
Business Manager portfolios the connected Meta user belongs to (Meta's `/me/businesses`),
rows returned verbatim (id, name, verification_status, created_time). Token-scoped, so no
`adAccountId` is needed. For TikTok Business Centers use
`GET /v1/ads/business-centers`.
### Parameters
- **accountId** (required) in query: Zernio SocialAccount id (posting or ads variant) used to resolve the Meta token.
- **limit** (optional) in query: Rows per page
- **after** (optional) in query: Cursor from paging.after of the previous page.
### Responses
#### 200: Businesses (raw Meta shape)
**Response Body:**
- **data** `array[object]`:
Type: `object`
- **paging** `object`:
- **after** `string,null`: Cursor for the next page; null when exhausted.
#### 400: Invalid input, or Meta rejected the query
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 501: Only supported on Meta (facebook/instagram)
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# List TikTok ad pixels API Reference
Lists pixels and their supported optimization events for a connected TikTok Ads account. The advertiser defaults to the first advertiser on the connection. Reconnect if Pixel Management permission has not been granted.
## GET /v1/ads/pixels
**List TikTok ad pixels**
Lists pixels and their supported optimization events for a connected TikTok Ads account. The advertiser defaults to the first advertiser on the connection. Reconnect if Pixel Management permission has not been granted.
### Parameters
- **accountId** (required) in query: Zernio SocialAccount ID.
- **advertiserId** (optional) in query: Advertiser belonging to this connection.
- **code** (optional) in query: Filter by a Pixel Code.
### Responses
#### 200: TikTok pixels.
**Response Body:**
- **advertiserId** `string`: No description
- **pixels** `array[object]`:
- **pixelId** `string`: No description
- **pixelCode** `string`: No description
- **name** `string`: No description
- **events** `array[string]`:
- **eventDetails** `array[object]`:
- **name** `string`: No description
- **optimizationEvent** `string,null`: No description
- **custom** `boolean`: No description
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 422: Pixel Management permission is missing (code reconnect_required). Reconnect TikTok Ads to grant it.
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# List value rule sets API Reference
Lists the ad account's value rule sets (Meta's `/act_X/value_rule_set`). A value rule
set adjusts the auction bid up or down for audience segments you value differently;
attach one to an ad set with `valueRuleSetId` on `POST /v1/ads/create` or
`PUT /v1/ads/ad-sets/{adSetId}`.
Rows are returned in the same camelCase shape the `PUT` body takes, ids included, so a
set round-trips 1:1: **the update is a full replace, not a patch**, so you GET, mutate
and send the whole thing back.
Limits: 6 rule sets per ad account, 10 rules per set, 4 criteria per rule.
**Rule order is semantic.** Rules are evaluated in array order and only the FIRST
matching rule adjusts the bid for an overlapping audience. The order you send is the
order that is stored and returned.
Eligibility: value rule sets apply only to ad sets on the `LOWEST_COST_WITHOUT_CAP`
(auto-bid) or `COST_CAP` bid strategies. Meta rejects the rest server-side.
## GET /v1/ads/value-rule-sets
**List value rule sets**
Lists the ad account's value rule sets (Meta's `/act_X/value_rule_set`). A value rule
set adjusts the auction bid up or down for audience segments you value differently;
attach one to an ad set with `valueRuleSetId` on `POST /v1/ads/create` or
`PUT /v1/ads/ad-sets/{adSetId}`.
Rows are returned in the same camelCase shape the `PUT` body takes, ids included, so a
set round-trips 1:1: **the update is a full replace, not a patch**, so you GET, mutate
and send the whole thing back.
Limits: 6 rule sets per ad account, 10 rules per set, 4 criteria per rule.
**Rule order is semantic.** Rules are evaluated in array order and only the FIRST
matching rule adjusts the bid for an overlapping audience. The order you send is the
order that is stored and returned.
Eligibility: value rule sets apply only to ad sets on the `LOWEST_COST_WITHOUT_CAP`
(auto-bid) or `COST_CAP` bid strategies. Meta rejects the rest server-side.
### Parameters
- **accountId** (required) in query: Zernio SocialAccount id (posting or ads variant) used to resolve the Meta token.
- **adAccountId** (required) in query: Meta ad account id (act_).
- **limit** (optional) in query: Rows per page
- **after** (optional) in query: Cursor from paging.after of the previous page. Meta does not document paging on this edge; `after` comes back null when it omits cursors.
### Responses
#### 200: Value rule sets
**Response Body:**
- **adAccountId** `string`: No description
- **data** `array[ValueRuleSet]`:
- **paging** `object`:
- **after** `string,null`: Cursor for the next page; null when exhausted or when Meta omits paging.
#### 400: Invalid input, or Meta rejected the query. Meta answers a bad rule-set id with GraphMethodException code 100 / subcode 33, which is indistinguishable between not-found, no-permission, and account-not-enabled.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 501: Only supported on Meta (facebook/instagram)
---
## POST /v1/ads/value-rule-sets
**Create a value rule set**
Creates a value rule set on the ad account (Meta's `POST /act_X/value_rule_set`).
Attach the returned id to an ad set with `valueRuleSetId` on `POST /v1/ads/create` or
`PUT /v1/ads/ad-sets/{adSetId}`.
**Rule order is semantic**: rules are evaluated in array order and only the first
matching rule adjusts the bid for an overlapping audience.
`adjustValue` is an unsigned magnitude in percent; the direction lives in `adjustSign`.
`INCREASE` accepts 1-1000, `DECREASE` accepts 1-90. There is no signed field and 0 is
out of range.
`criteriaValueTypes` is positionally paired with `criteriaValues` (same length, same
order). Every type is the literal `"NONE"` except on `LOCATION`, which uses
`LOCATION_COUNTRY` / `LOCATION_REGION` / `LOCATION_CITY` / `LOCATION_COMSCORE_MARKET`
and may mix them within one criterion. Location values are Targeting-Search keys: a
two-letter country code for `LOCATION_COUNTRY`, a numeric key for the rest.
`LOCATION_DMA` was replaced by `LOCATION_COMSCORE_MARKET` on 2026-06-22 and rules using
DMAs are no longer active, so this API rejects it.
`AUDIENCE_LABEL` values (e.g. `HIGH_VALUE`) are applied to a Custom Audience in Ads
Manager. There is no API to provision them, so label strings are passed through
unvalidated and a typo produces a rule that never fires.
Ads Manager turns a rule set read-only (this API stays editable) when a rule uses more
than 2 criteria, a custom age range, or the placements `FB_MARKETPLACE`, `FB_SEARCH`,
`FB_VIDEO` or `IG_EXPLORE`.
Limits: 6 rule sets per ad account, 10 rules per set, 4 criteria per rule. The
per-account cap is enforced by Meta, not here.
### Request Body
- **accountId** (required) `string`: Zernio SocialAccount id (posting or ads variant); its platform decides where the campaign is created.
- **adAccountId** (required) `string`: Platform ad account id (Meta act_, Google customer id, LinkedIn account id, ...).
- **name** (required) `string`: No description
- **rules** (required) `array`: Evaluated in order; the first matching rule wins.
### Responses
#### 201: Value rule set created
**Response Body:**
- **adAccountId** `string`: No description
- **valueRuleSetId** `string,null`: The new rule set id. Meta does not document the create response body, so this is null on the (unobserved) case where it omits the id.
#### 400: Invalid input, or Meta rejected the create (per-account rule-set cap, ineligible criteria, or an account that is not enabled for value rules)
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 501: Only supported on Meta (facebook/instagram)
---
# Related Schema Definitions
## ValueRuleSet
A named set of bid-adjustment rules on an ad account. Attach it to an ad set with
`valueRuleSetId`. Limits: 6 sets per ad account, 10 rules per set, 4 criteria per rule.
### Properties
- **id** (required) `string`: Platform value rule set id.
- **name** (required) `string`: No description
- **rules** (required) `array`: Evaluated in order; the first matching rule wins.
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Remove account callout API Reference
Removes the customer_asset attachment only. The underlying shared asset and its campaign or ad-group attachments remain.
## GET /v1/ads/accounts/callouts
**List account callouts**
Lists directly attached Google assets. Fresh reads are cached for 10 minutes; exhausted quota may return the last successful read with stale=true. Inherited assets are not included. Preserves Google RMF C.75 account-level callouts.
### Parameters
- **accountId** (required) in query: No description
- **customerId** (optional) in query: No description
### Responses
#### 200: Assets returned.
**Response Body:**
- **customerId** `string`: No description
- **callouts** `array[object]`:
- **assetId** `string`: No description
- **status** `string`: No description
- **text** `string`: No description
- **cachedAt** `string,null` (date-time): Time of the cached Google read. Null when no cache was used.
- **stale** `boolean`: True when exhausted quota required returning the last successful read.
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access is required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Only supported on Google Ads.
---
## POST /v1/ads/accounts/callouts
**Add account callouts**
Creates assets and customer_asset links for this Google customer. Links apply at account level.
### Request Body
- **accountId** (required) `string`: Zernio Google Ads connection id.
- **customerId** `string`: Google customer id without dashes. Required when the connection has multiple customers.
- **callouts** (required) `array`: No description
### Responses
#### 201: Assets created and attached.
**Response Body:**
- **customerId** `string`: No description
- **callouts** `array[object]`:
- **assetId** `string`: No description
- **text** `string`: No description
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access is required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Only supported on Google Ads.
---
## PUT /v1/ads/accounts/callouts
**Update account callouts**
Edits existing Google assets in place. Send updates with assetResourceName and the fields to change. An asset is shared: changes affect every attachment using it. Omitted fields stay unchanged. The operation consumes the Google operations budget and invalidates affected cached lists.
### Request Body
- **accountId** (required) `string`: Zernio Google Ads connection id.
- **customerId** `string`: Google customer id without dashes. Required when the connection has multiple customers.
- **updates** (required) `array`: No description
### Responses
#### 200: Assets returned.
**Response Body:**
- **updated** `integer`: No description
- **customerId** `string`: No description
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access is required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Only supported on Google Ads.
---
## DELETE /v1/ads/accounts/callouts
**Remove account callout**
Removes the customer_asset attachment only. The underlying shared asset and its campaign or ad-group attachments remain.
### Request Body
- **accountId** (required) `string`: Zernio Google Ads connection id.
- **customerId** `string`: Google customer id without dashes. Required when the connection has multiple customers.
- **assetId** (required) `string`: No description
### Responses
#### 200: Assets returned.
**Response Body:**
- **removed** `boolean`: No description
- **customerId** `string`: No description
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access is required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Only supported on Google Ads.
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Remove account sitelink API Reference
Removes the customer_asset attachment only. The underlying shared asset and its campaign or ad-group attachments remain.
## GET /v1/ads/accounts/sitelinks
**List account sitelinks**
Lists directly attached Google assets. Fresh reads are cached for 10 minutes; exhausted quota may return the last successful read with stale=true. Inherited assets are not included.
### Parameters
- **accountId** (required) in query: No description
- **customerId** (optional) in query: No description
### Responses
#### 200: Assets returned.
**Response Body:**
- **customerId** `string`: No description
- **sitelinks** `array[object]`:
- **assetId** `string`: No description
- **status** `string`: No description
- **assetResourceName** `string`: No description
- **customerAssetResourceName** `string`: No description
- **text** `string`: No description
- **linkUrl** `string` (uri): No description
- **description1** `string`: No description
- **description2** `string`: No description
- **cachedAt** `string,null` (date-time): Time of the cached Google read. Null when no cache was used.
- **stale** `boolean`: True when exhausted quota required returning the last successful read.
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access is required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Only supported on Google Ads.
---
## POST /v1/ads/accounts/sitelinks
**Add account sitelinks**
Creates assets and customer_asset links for this Google customer. Links apply at account level.
### Request Body
- **accountId** (required) `string`: Zernio Google Ads connection id.
- **customerId** `string`: Google customer id without dashes. Required when the connection has multiple customers.
- **sitelinks** (required) `array`: No description
### Responses
#### 201: Assets created and attached.
**Response Body:**
- **customerId** `string`: No description
- **sitelinks** `array[object]`:
- **assetId** `string`: No description
- **text** `string`: No description
- **linkUrl** `string` (uri): No description
- **description1** `string`: No description
- **description2** `string`: No description
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access is required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Only supported on Google Ads.
---
## PUT /v1/ads/accounts/sitelinks
**Update account sitelinks**
Edits existing Google assets in place. Send updates with assetResourceName and the fields to change. An asset is shared: changes affect every attachment using it. Omitted fields stay unchanged. The operation consumes the Google operations budget and invalidates affected cached lists.
### Request Body
- **accountId** (required) `string`: Zernio Google Ads connection id.
- **customerId** `string`: Google customer id without dashes. Required when the connection has multiple customers.
- **updates** (required) `array`: No description
### Responses
#### 200: Assets returned.
**Response Body:**
- **updated** `integer`: No description
- **customerId** `string`: No description
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access is required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Only supported on Google Ads.
---
## DELETE /v1/ads/accounts/sitelinks
**Remove account sitelink**
Removes the customer_asset attachment only. The underlying shared asset and its campaign or ad-group attachments remain.
### Request Body
- **accountId** (required) `string`: Zernio Google Ads connection id.
- **customerId** `string`: Google customer id without dashes. Required when the connection has multiple customers.
- **assetId** (required) `string`: No description
### Responses
#### 200: Assets returned.
**Response Body:**
- **removed** `boolean`: No description
- **customerId** `string`: No description
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access is required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Only supported on Google Ads.
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Remove account snippet API Reference
Removes the customer_asset attachment only. The underlying shared asset and its campaign or ad-group attachments remain.
## GET /v1/ads/accounts/structured-snippets
**List account snippets**
Lists directly attached Google assets. Fresh reads are cached for 10 minutes; exhausted quota may return the last successful read with stale=true. Inherited assets are not included.
### Parameters
- **accountId** (required) in query: No description
- **customerId** (optional) in query: No description
### Responses
#### 200: Assets returned.
**Response Body:**
- **customerId** `string`: No description
- **structuredSnippets** `array[object]`:
- **assetId** `string`: No description
- **status** `string`: No description
- **assetResourceName** `string`: No description
- **customerAssetResourceName** `string`: No description
- **header** `string`: No description
- **values** `array[string]`:
- **cachedAt** `string,null` (date-time): Time of the cached Google read. Null when no cache was used.
- **stale** `boolean`: True when exhausted quota required returning the last successful read.
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access is required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Only supported on Google Ads.
---
## POST /v1/ads/accounts/structured-snippets
**Add account snippets**
Creates assets and customer_asset links for this Google customer. Links apply at account level.
### Request Body
- **accountId** (required) `string`: Zernio Google Ads connection id.
- **customerId** `string`: Google customer id without dashes. Required when the connection has multiple customers.
- **structuredSnippets** (required) `array`: No description
### Responses
#### 201: Assets created and attached.
**Response Body:**
- **customerId** `string`: No description
- **structuredSnippets** `array[object]`:
- **assetId** `string`: No description
- **header** `string`: No description - one of: Amenities, Brands, Courses, Degree programs, Destinations, Featured hotels, Insurance coverage, Models, Neighborhoods, Service catalog, Shows, Styles, Types
- **values** `array[string]`:
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access is required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Only supported on Google Ads.
---
## PUT /v1/ads/accounts/structured-snippets
**Update account snippets**
Edits existing Google assets in place. Send updates with assetResourceName and the fields to change. An asset is shared: changes affect every attachment using it. Omitted fields stay unchanged. The operation consumes the Google operations budget and invalidates affected cached lists.
### Request Body
- **accountId** (required) `string`: Zernio Google Ads connection id.
- **customerId** `string`: Google customer id without dashes. Required when the connection has multiple customers.
- **updates** (required) `array`: No description
### Responses
#### 200: Assets returned.
**Response Body:**
- **updated** `integer`: No description
- **customerId** `string`: No description
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access is required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Only supported on Google Ads.
---
## DELETE /v1/ads/accounts/structured-snippets
**Remove account snippet**
Removes the customer_asset attachment only. The underlying shared asset and its campaign or ad-group attachments remain.
### Request Body
- **accountId** (required) `string`: Zernio Google Ads connection id.
- **customerId** `string`: Google customer id without dashes. Required when the connection has multiple customers.
- **assetId** (required) `string`: No description
### Responses
#### 200: Assets returned.
**Response Body:**
- **removed** `boolean`: No description
- **customerId** `string`: No description
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access is required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Only supported on Google Ads.
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Replace negative list keywords API Reference
Replaces the full desired keyword set. Existing keywords are diffed by normalized text and match type; creates and removals are applied atomically in one mutation. Unchanged criteria retain their ids. Send an empty keywords array to clear the list. Changes affect every campaign using this list. Each create or removal consumes one daily operation; the entire batch must fit the remaining quota.
## PUT /v1/ads/accounts/negative-keyword-lists/{listId}/keywords
**Replace negative list keywords**
Replaces the full desired keyword set. Existing keywords are diffed by normalized text and match type; creates and removals are applied atomically in one mutation. Unchanged criteria retain their ids. Send an empty keywords array to clear the list. Changes affect every campaign using this list. Each create or removal consumes one daily operation; the entire batch must fit the remaining quota.
### Parameters
- **listId** (required) in path: No description
### Request Body
- **accountId** (required) `string`: Zernio SocialAccount id.
- **customerId** `string`: Connected Google Ads customer id, without dashes. Required when the connection has multiple customers.
- **platform** `string`: Optional courtesy field. The resolved account or campaign determines support; other platforms return 501. - one of: facebook, instagram, tiktok, linkedin, pinterest, google, twitter, openai
- **keywords** (required) `array`: Full desired keyword set. Bare strings use broad match. Send [] to clear the list.
### Responses
#### 200: Successful response.
**Response Body:**
- **created** `integer`: New criteria or campaign links created.
- **removed** `integer`: Existing criteria or campaign links removed.
- **customerId** `string`: Resolved Google Ads customer id.
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access and permission to the selected account are required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: Ambiguous campaign or account selection. Use a profile-scoped key. A list still attached to a campaign may also be rejected by Google. The account may also be inactive or need reconnection (code ads_connection_required). Reconnect it and read GET /v1/accounts for its current ID before retrying.
#### 422: Google Ads connection is missing or unavailable.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Available only on Google Ads.
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Reply to an ad comment API Reference
Reply to a first-level TikTok ad comment. Requires a TT_USER or CUSTOMIZED_USER identity with comment-management permission. Replies to replies are rejected. The response commentId identifies the new reply. This operation is not idempotent; do not blindly retry an uncertain response.
Unknown identity and video item fields are resolved only when needed for this
action, then persisted for reuse. Comment-specific fields take precedence.
If TikTok no longer returns the ad needed to resolve identity, 404 ad_not_found
directs you to check deletion or archival in TikTok Ads Manager. Listing can
still succeed. Unsupported or unavailable identity returns 403 feature_not_available.
Denied access to ad details returns 403 insufficient_permissions with reconnect
guidance and the upstream platformError.
Requires Ads access. The ad is resolved within the caller's accessible profiles.
Before moderation, Zernio verifies that the comment belongs to this ad using
TikTok's ad-group comment listing. The default search window is the last 30 days.
Use since/until for older comments, with at most 30 days between the dates.
Lookups scan at most 2,000 ad-group comments; narrow the date window if exceeded.
Meta returns 501 feature_not_available with guidance to use the existing inbox
comment endpoints and the account/post IDs from GET /v1/ads/{adId}/comments.
## POST /v1/ads/{adId}/comments/{commentId}/reply
**Reply to an ad comment**
Reply to a first-level TikTok ad comment. Requires a TT_USER or CUSTOMIZED_USER identity with comment-management permission. Replies to replies are rejected. The response commentId identifies the new reply. This operation is not idempotent; do not blindly retry an uncertain response.
Unknown identity and video item fields are resolved only when needed for this
action, then persisted for reuse. Comment-specific fields take precedence.
If TikTok no longer returns the ad needed to resolve identity, 404 ad_not_found
directs you to check deletion or archival in TikTok Ads Manager. Listing can
still succeed. Unsupported or unavailable identity returns 403 feature_not_available.
Denied access to ad details returns 403 insufficient_permissions with reconnect
guidance and the upstream platformError.
Requires Ads access. The ad is resolved within the caller's accessible profiles.
Before moderation, Zernio verifies that the comment belongs to this ad using
TikTok's ad-group comment listing. The default search window is the last 30 days.
Use since/until for older comments, with at most 30 days between the dates.
Lookups scan at most 2,000 ad-group comments; narrow the date window if exceeded.
Meta returns 501 feature_not_available with guidance to use the existing inbox
comment endpoints and the account/post IDs from GET /v1/ads/{adId}/comments.
### Parameters
- **adId** (required) in path: Internal Zernio ad ID or indexed platform ad ID.
- **commentId** (required) in path: TikTok comment ID from the ad comment listing.
- **since** (optional) in query: Start date of the comment lookup window. Defaults to 30 days before until.
- **until** (optional) in query: End date of the comment lookup window. Defaults to today in UTC.
### Request Body
- **text** (required) `string`: Non-empty reply text.
### Responses
#### 200: Comment action completed.
**Response Body:**
- **status** (required) `string`: No description - one of: success
- **commentId** (required) `string`: ID of the created reply or moderated comment.
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access or supported identity is unavailable (feature_not_available), or TikTok denies ad-detail access or comment-management permission (insufficient_permissions). Grant permission and reconnect the TikTok Ads account before retrying.
#### 404: Ad is inaccessible or unavailable on TikTok for identity resolution (ad_not_found), or the comment was not found on this ad in the selected date window (resource_not_found).
#### 422: TikTok Ads connection is unavailable.
#### 501: Moderation on this route supports TikTok. Use the inbox comment routes for Meta.
#### 502: TikTok rejected the request or was unavailable. Inspect platformError for its code and message.
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Update account callouts API Reference
Edits existing Google assets in place. Send updates with assetResourceName and the fields to change. An asset is shared: changes affect every attachment using it. Omitted fields stay unchanged. The operation consumes the Google operations budget and invalidates affected cached lists.
## GET /v1/ads/accounts/callouts
**List account callouts**
Lists directly attached Google assets. Fresh reads are cached for 10 minutes; exhausted quota may return the last successful read with stale=true. Inherited assets are not included. Preserves Google RMF C.75 account-level callouts.
### Parameters
- **accountId** (required) in query: No description
- **customerId** (optional) in query: No description
### Responses
#### 200: Assets returned.
**Response Body:**
- **customerId** `string`: No description
- **callouts** `array[object]`:
- **assetId** `string`: No description
- **status** `string`: No description
- **text** `string`: No description
- **cachedAt** `string,null` (date-time): Time of the cached Google read. Null when no cache was used.
- **stale** `boolean`: True when exhausted quota required returning the last successful read.
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access is required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Only supported on Google Ads.
---
## POST /v1/ads/accounts/callouts
**Add account callouts**
Creates assets and customer_asset links for this Google customer. Links apply at account level.
### Request Body
- **accountId** (required) `string`: Zernio Google Ads connection id.
- **customerId** `string`: Google customer id without dashes. Required when the connection has multiple customers.
- **callouts** (required) `array`: No description
### Responses
#### 201: Assets created and attached.
**Response Body:**
- **customerId** `string`: No description
- **callouts** `array[object]`:
- **assetId** `string`: No description
- **text** `string`: No description
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access is required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Only supported on Google Ads.
---
## PUT /v1/ads/accounts/callouts
**Update account callouts**
Edits existing Google assets in place. Send updates with assetResourceName and the fields to change. An asset is shared: changes affect every attachment using it. Omitted fields stay unchanged. The operation consumes the Google operations budget and invalidates affected cached lists.
### Request Body
- **accountId** (required) `string`: Zernio Google Ads connection id.
- **customerId** `string`: Google customer id without dashes. Required when the connection has multiple customers.
- **updates** (required) `array`: No description
### Responses
#### 200: Assets returned.
**Response Body:**
- **updated** `integer`: No description
- **customerId** `string`: No description
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access is required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Only supported on Google Ads.
---
## DELETE /v1/ads/accounts/callouts
**Remove account callout**
Removes the customer_asset attachment only. The underlying shared asset and its campaign or ad-group attachments remain.
### Request Body
- **accountId** (required) `string`: Zernio Google Ads connection id.
- **customerId** `string`: Google customer id without dashes. Required when the connection has multiple customers.
- **assetId** (required) `string`: No description
### Responses
#### 200: Assets returned.
**Response Body:**
- **removed** `boolean`: No description
- **customerId** `string`: No description
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access is required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Only supported on Google Ads.
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Update account sitelinks API Reference
Edits existing Google assets in place. Send updates with assetResourceName and the fields to change. An asset is shared: changes affect every attachment using it. Omitted fields stay unchanged. The operation consumes the Google operations budget and invalidates affected cached lists.
## GET /v1/ads/accounts/sitelinks
**List account sitelinks**
Lists directly attached Google assets. Fresh reads are cached for 10 minutes; exhausted quota may return the last successful read with stale=true. Inherited assets are not included.
### Parameters
- **accountId** (required) in query: No description
- **customerId** (optional) in query: No description
### Responses
#### 200: Assets returned.
**Response Body:**
- **customerId** `string`: No description
- **sitelinks** `array[object]`:
- **assetId** `string`: No description
- **status** `string`: No description
- **assetResourceName** `string`: No description
- **customerAssetResourceName** `string`: No description
- **text** `string`: No description
- **linkUrl** `string` (uri): No description
- **description1** `string`: No description
- **description2** `string`: No description
- **cachedAt** `string,null` (date-time): Time of the cached Google read. Null when no cache was used.
- **stale** `boolean`: True when exhausted quota required returning the last successful read.
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access is required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Only supported on Google Ads.
---
## POST /v1/ads/accounts/sitelinks
**Add account sitelinks**
Creates assets and customer_asset links for this Google customer. Links apply at account level.
### Request Body
- **accountId** (required) `string`: Zernio Google Ads connection id.
- **customerId** `string`: Google customer id without dashes. Required when the connection has multiple customers.
- **sitelinks** (required) `array`: No description
### Responses
#### 201: Assets created and attached.
**Response Body:**
- **customerId** `string`: No description
- **sitelinks** `array[object]`:
- **assetId** `string`: No description
- **text** `string`: No description
- **linkUrl** `string` (uri): No description
- **description1** `string`: No description
- **description2** `string`: No description
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access is required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Only supported on Google Ads.
---
## PUT /v1/ads/accounts/sitelinks
**Update account sitelinks**
Edits existing Google assets in place. Send updates with assetResourceName and the fields to change. An asset is shared: changes affect every attachment using it. Omitted fields stay unchanged. The operation consumes the Google operations budget and invalidates affected cached lists.
### Request Body
- **accountId** (required) `string`: Zernio Google Ads connection id.
- **customerId** `string`: Google customer id without dashes. Required when the connection has multiple customers.
- **updates** (required) `array`: No description
### Responses
#### 200: Assets returned.
**Response Body:**
- **updated** `integer`: No description
- **customerId** `string`: No description
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access is required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Only supported on Google Ads.
---
## DELETE /v1/ads/accounts/sitelinks
**Remove account sitelink**
Removes the customer_asset attachment only. The underlying shared asset and its campaign or ad-group attachments remain.
### Request Body
- **accountId** (required) `string`: Zernio Google Ads connection id.
- **customerId** `string`: Google customer id without dashes. Required when the connection has multiple customers.
- **assetId** (required) `string`: No description
### Responses
#### 200: Assets returned.
**Response Body:**
- **removed** `boolean`: No description
- **customerId** `string`: No description
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access is required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Only supported on Google Ads.
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Update account snippets API Reference
Edits existing Google assets in place. Send updates with assetResourceName and the fields to change. An asset is shared: changes affect every attachment using it. Omitted fields stay unchanged. The operation consumes the Google operations budget and invalidates affected cached lists.
## GET /v1/ads/accounts/structured-snippets
**List account snippets**
Lists directly attached Google assets. Fresh reads are cached for 10 minutes; exhausted quota may return the last successful read with stale=true. Inherited assets are not included.
### Parameters
- **accountId** (required) in query: No description
- **customerId** (optional) in query: No description
### Responses
#### 200: Assets returned.
**Response Body:**
- **customerId** `string`: No description
- **structuredSnippets** `array[object]`:
- **assetId** `string`: No description
- **status** `string`: No description
- **assetResourceName** `string`: No description
- **customerAssetResourceName** `string`: No description
- **header** `string`: No description
- **values** `array[string]`:
- **cachedAt** `string,null` (date-time): Time of the cached Google read. Null when no cache was used.
- **stale** `boolean`: True when exhausted quota required returning the last successful read.
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access is required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Only supported on Google Ads.
---
## POST /v1/ads/accounts/structured-snippets
**Add account snippets**
Creates assets and customer_asset links for this Google customer. Links apply at account level.
### Request Body
- **accountId** (required) `string`: Zernio Google Ads connection id.
- **customerId** `string`: Google customer id without dashes. Required when the connection has multiple customers.
- **structuredSnippets** (required) `array`: No description
### Responses
#### 201: Assets created and attached.
**Response Body:**
- **customerId** `string`: No description
- **structuredSnippets** `array[object]`:
- **assetId** `string`: No description
- **header** `string`: No description - one of: Amenities, Brands, Courses, Degree programs, Destinations, Featured hotels, Insurance coverage, Models, Neighborhoods, Service catalog, Shows, Styles, Types
- **values** `array[string]`:
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access is required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Only supported on Google Ads.
---
## PUT /v1/ads/accounts/structured-snippets
**Update account snippets**
Edits existing Google assets in place. Send updates with assetResourceName and the fields to change. An asset is shared: changes affect every attachment using it. Omitted fields stay unchanged. The operation consumes the Google operations budget and invalidates affected cached lists.
### Request Body
- **accountId** (required) `string`: Zernio Google Ads connection id.
- **customerId** `string`: Google customer id without dashes. Required when the connection has multiple customers.
- **updates** (required) `array`: No description
### Responses
#### 200: Assets returned.
**Response Body:**
- **updated** `integer`: No description
- **customerId** `string`: No description
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access is required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Only supported on Google Ads.
---
## DELETE /v1/ads/accounts/structured-snippets
**Remove account snippet**
Removes the customer_asset attachment only. The underlying shared asset and its campaign or ad-group attachments remain.
### Request Body
- **accountId** (required) `string`: Zernio Google Ads connection id.
- **customerId** `string`: Google customer id without dashes. Required when the connection has multiple customers.
- **assetId** (required) `string`: No description
### Responses
#### 200: Assets returned.
**Response Body:**
- **removed** `boolean`: No description
- **customerId** `string`: No description
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access is required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Only supported on Google Ads.
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Update ad account settings API Reference
Sets the default DSA beneficiary and payor on a Meta ad account (EU DSA, Article 26).
Set them once and every EU-targeted call to `/v1/ads/create`, `/v1/ads/boost` and
`/v1/ads/ctwa` on that ad account can omit `dsaBeneficiary`/`dsaPayor`: Meta applies
the defaults automatically.
The values are written to the ad account on Meta, the same setting Ads Manager edits.
Nothing is stored in Zernio, and defaults already set in Ads Manager work identically.
Zernio never guesses these values for you. Beneficiary and payor are legal disclosures
shown to EU users, so you must provide the entity names explicitly. Use
`GET /v1/ads/dsa-recommendations` to offer suggestions in your UI.
If `defaultDsaPayor` is omitted, the beneficiary is also set as the payor, which
covers the common case where the same entity benefits from and pays for the ads.
Read the current values back with `GET /v1/ads/dsa-defaults`.
Currently supported for Meta accounts only; other platforms return 400.
## POST /v1/ads/accounts
**Create Meta ad account**
Creates a durable Meta ad account in the end user's own business portfolio using
their connected Meta Ads token. Requires an active metaads accountId, Ads access,
business_management permission and business admin access. Discover portfolios with
GET /v1/ads/businesses. System-user tokens may return an empty businesses list;
supply the known business ID in that case.
The self-serve account starts without a payment method. The user must add a payment
method in Ads Manager before ads can deliver. Zernio cannot add payment methods.
Meta may require business verification and limits how many accounts a business can
create. Closing an account does not guarantee more capacity. An ad account cannot
truly be deleted, even after closing it and removing it from a business.
timezoneId is Meta's numeric ID, not an IANA timezone name. Select it from
https://developers.facebook.com/docs/marketing-api/reference/ad-account/timezone-ids/.
For example, 1 is America/Los_Angeles. Meta validates supported currencies and IDs.
endAdvertiser, mediaAgency and partner default to NONE for the self-serve flow.
The new account is added atomically to an existing scoped ad-account allowlist.
Unrestricted connections stay unrestricted. Reconnecting the same Meta identity
preserves this scope unless a caller explicitly replaces it. Discovery is nudged
immediately. Use the returned adAccountId with the existing ads endpoints.
This operation is not idempotent and Zernio never automatically retries it.
Unknown body fields are rejected. No validateOnly or dry-run option is supported.
After a timeout or a 502 with details.creationStatus=unknown, check the business
in Ads Manager before attempting another creation. A 201 with connectionUpdated=false
means the account exists but needs reconnecting with adAccountIds containing the returned ID and the previous
scoped IDs via GET /v1/connect/facebook/ads. Do not repeat the create call.
### Request Body
- **accountId** (required) `string`: Zernio metaads SocialAccount ID.
- **businessId** (required) `string`: Business portfolio that will own the account.
- **name** (required) `string`: Ad account name. Whitespace is trimmed.
- **currency** (required) `string`: Uppercase ISO 4217 currency supported by Meta.
- **timezoneId** (required) `integer`: Numeric Meta timezone ID from the linked timezone list. For example 1 is America/Los_Angeles.
- **endAdvertiser** `string`: End advertiser business or page ID. NONE uses the owning business.
- **mediaAgency** `string`: Media agency business or page ID. NONE for self-serve customers.
- **partner** `string`: Partner business or page ID. NONE for self-serve customers.
- **invoice** `boolean`: Request Meta invoicing. Eligibility is determined by Meta.
- **invoiceGroupId** `string`: Existing Meta invoice group ID.
- **invoicingEmails** `array`: Addresses for Meta invoices.
- **io** `boolean`: Meta insertion-order invoicing option.
- **poNumber** `string`: Purchase order number.
- **fundingId** `string`: Existing Meta funding reference. Does not add a payment method.
- **adAccountCreatedFromBmFlag** `boolean`: Meta Business Manager creation flag.
### Responses
#### 201: Ad account created. Check connectionUpdated and payment instructions.
**Response Body:**
- **adAccountId** (required) `string`: New Meta ad account ID for subsequent ads calls.
- **businessId** (required) `string`: Owning business portfolio ID.
- **connectionUpdated** (required) `boolean`: Whether the connection scope and discovery schedule were updated.
- **paymentMethodRequired** (required) `boolean`: Always true as a delivery prerequisite. This is not a live funding-source check. Confirm payment or invoicing in Ads Manager.
- **adsManagerUrl** (required) `string` (uri): Open the created account in Ads Manager.
- **nextSteps** (required) `string`: Payment setup instructions for the user.
- **warnings** (required) `array[string]`: Recovery instructions if the account could not be attached to the connection.
#### 400: Invalid input or Meta rejection. details.reason identifies creation_limit, business_verification_required, unsupported_currency, unsupported_timezone or business_unavailable when recognized.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access denied or Meta permission missing. details.reason may be business_management_required, business_admin_required or business_access_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 502: Creation outcome unknown. Check Ads Manager before repeating this non-idempotent request.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
## GET /v1/ads/accounts
**List ad accounts**
Returns the platform ad accounts available for the given account (e.g. Meta ad
accounts, TikTok advertiser IDs, Google Ads customer IDs).
Meta business-login accounts use their own system-user token. Fresh Meta discovery
includes businessId and businessName from the owning Business Manager when available;
cached entries gain these fields after the next discovery refresh.
For TikTok agencies: enumerates every advertiser under every Business Center the token
can read (paginated server-side), then chunks the lookup against TikTok's
`/advertiser/info/` endpoint (which has a per-call cap of ≤100 IDs). Solo advertisers
without a BC fall back to the OAuth-time `advertiser_ids` list. Cached for 1h on the
SocialAccount; lazy-refreshed on first call after expiry.
For Google Ads: responds `429` when Google's API quota is temporarily exhausted
(instead of an empty list). Retry after a delay.
### Parameters
- **accountId** (required) in query: Account ID
- **adAccountId** (optional) in query: Filter response to a single platform ad account ID (e.g. `act_123` for Meta, advertiser_id for TikTok). Returns at most one item.
- **limit** (optional) in query: Clamp the returned `accounts[]` length. Useful for typeahead pickers on agency tokens with hundreds of advertisers.
### Responses
#### 200: Ad accounts
**Response Body:**
- **accounts** `array[object]`:
- **id** `string`: Platform ad account ID (e.g. act_123)
- **name** `string`: No description
- **currency** `string`: No description
- **businessId** `string`: Meta only. Owning Business Manager ID when available on the grant.
- **businessName** `string`: Owning business name when supplied by the platform.
- **status** `string`: LinkedIn only. LinkedIn's own ad account status. In practice always `ACTIVE`, because the LinkedIn query filters to active accounts. Meta, Google, TikTok and Pinterest report `accountStatus` instead; X reports `approvalStatus`.
- **accountStatus**: The platform's own account status, forwarded unchanged. No JSON type is
declared because the type differs per platform: Meta sends an integer,
Google, TikTok and Pinterest send a string. Absent on LinkedIn (reports
`status`) and on X (reports `approvalStatus`).
If all you need is whether the account can run ads right now, read
`selectable` and skip this field. Read this one when you need to tell
the states apart, because they call for different responses:
- `1` ACTIVE. Running normally.
- `2` DISABLED. Disabled by Meta. Read `disableReason` to tell a policy
action apart from a billing one; they need very different follow-ups.
- `3` UNSETTLED. There is an unpaid balance, but the account still runs
ads. Not a ban.
- `7` PENDING_RISK_REVIEW. Meta is reviewing the account. Wait for the
outcome.
- `8` PENDING_SETTLEMENT. Meta blocks new ads until an outstanding
balance clears. Settle it and the account runs again.
- `9` IN_GRACE_PERIOD. Still running, on a deadline.
- `100` PENDING_CLOSURE. Scheduled to close.
- `101` CLOSED. Terminal.
- **approvalStatus** `string`: X only. X's own ad account approval status. Observed values are `ACCEPTED`, `PENDING` and `REJECTED`, but X does not publish the full vocabulary, so treat an unrecognised value as not usable. Other platforms report `accountStatus` or `status` instead.
- **disableReason** `integer`: Meta only. Meta's `disable_reason` code, forwarded unchanged. Present when `accountStatus` is `2` (DISABLED) and Meta gives a reason, which is what separates a policy action from a payment problem. Meta does not publish a stable list of values for this field, so none are enumerated here: resolve the code against Meta's own ad account reference. Absent when Meta reports no reason, or when the connected token cannot read the field.
- **timezoneName** `string`: IANA timezone of the ad account (Meta only). Drives daily-budget reset and Insights day boundaries.
- **timezoneOffsetHoursUtc** `number`: Signed UTC offset in hours, reflecting current DST (Meta only).
- **minimumDailyBudget** `number`: Meta only. Minimum daily budget for the account, in the account currency's major units. This is the impressions-billed minimum; other billing events have higher minimums. Absent when the connected token cannot read it.
- **selectable** `boolean`: Meta and X only. Whether the account can create/run ads now. Absent (treat as true) on other platforms.
- **unusableReason** `string,null`: Meta and X only. Human-readable reason when selectable is false; null when selectable.
- **cachedAt** `string,null` (date-time): Google only. When this list was fetched from Google. Null when it was never served from cache, or on other platforms.
- **stale** `boolean`: Google only. True when Google's daily API quota was exhausted and this is the last successful fetch, not a live read. Absent on other platforms.
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 422: Platform ads connection required (TikTok Ads, X Ads) or Instagram missing linked Facebook account
#### 429: The connected account's upstream platform quota is exhausted.
Reddit rate-limits per connected Reddit user (1000 requests per
10-minute window), and that budget is shared by every operation using
that account. Retry after the window resets rather than retrying
immediately; repeated calls while exhausted do not succeed and keep the
budget spent.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
## PATCH /v1/ads/accounts
**Update ad account settings**
Sets the default DSA beneficiary and payor on a Meta ad account (EU DSA, Article 26).
Set them once and every EU-targeted call to `/v1/ads/create`, `/v1/ads/boost` and
`/v1/ads/ctwa` on that ad account can omit `dsaBeneficiary`/`dsaPayor`: Meta applies
the defaults automatically.
The values are written to the ad account on Meta, the same setting Ads Manager edits.
Nothing is stored in Zernio, and defaults already set in Ads Manager work identically.
Zernio never guesses these values for you. Beneficiary and payor are legal disclosures
shown to EU users, so you must provide the entity names explicitly. Use
`GET /v1/ads/dsa-recommendations` to offer suggestions in your UI.
If `defaultDsaPayor` is omitted, the beneficiary is also set as the payor, which
covers the common case where the same entity benefits from and pays for the ads.
Read the current values back with `GET /v1/ads/dsa-defaults`.
Currently supported for Meta accounts only; other platforms return 400.
### Request Body
- **accountId** (required) `string`: Account ID (metaads, or a facebook/instagram posting account)
- **adAccountId** (required) `string`: Meta ad account ID (act_...)
- **defaultDsaBeneficiary** (required) `string`: Legal entity benefiting from ads on this ad account
- **defaultDsaPayor** `string`: Legal entity paying for ads on this ad account. Defaults to defaultDsaBeneficiary when omitted.
### Responses
#### 200: DSA defaults updated (re-read from Meta after the write)
**Response Body:**
- **adAccountId** `string`: No description
- **dsaDefaults** `object`:
- **beneficiary** `string`: No description
- **payor** `string`: No description
#### 400: Unsupported platform (non-Meta account) or invalid adAccountId
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Rename a negative keyword list API Reference
Renames a shared negative keyword list. Keywords and campaign associations are unchanged. Use the keywords endpoint to edit the desired keyword set.
## GET /v1/ads/accounts/negative-keyword-lists/{listId}
**Get a negative keyword list**
Google Ads shared negative keyword lists (shared_set type NEGATIVE_KEYWORDS). Reads are cached for 10 minutes; quota exhaustion may return the last successful result for up to 7 days with stale=true. Customer selection is limited to this connection and its account scope. Includes the keywords and their criterion ids.
### Parameters
- **listId** (required) in path: No description
- **accountId** (required) in query: No description
- **customerId** (optional) in query: No description
- **platform** (optional) in query: No description
### Responses
#### 200: Successful response.
**Response Body:**
- **list**: No description
- **customerId** `string`: Resolved Google Ads customer id.
- **cachedAt** `string,null` (date-time): Last successful fetch time, or null without cache storage.
- **stale** `boolean`: True when quota exhaustion caused the last successful cached result to be served.
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access and permission to the selected account are required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: Ambiguous campaign or account selection. Use a profile-scoped key. A list still attached to a campaign may also be rejected by Google. The account may also be inactive or need reconnection (code ads_connection_required). Reconnect it and read GET /v1/accounts for its current ID before retrying.
#### 422: Google Ads connection is missing or unavailable.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Available only on Google Ads.
---
## PUT /v1/ads/accounts/negative-keyword-lists/{listId}
**Rename a negative keyword list**
Renames a shared negative keyword list. Keywords and campaign associations are unchanged. Use the keywords endpoint to edit the desired keyword set.
### Parameters
- **listId** (required) in path: No description
### Request Body
- **accountId** (required) `string`: Zernio SocialAccount id.
- **customerId** `string`: Connected Google Ads customer id, without dashes. Required when the connection has multiple customers.
- **platform** `string`: Optional courtesy field. The resolved account or campaign determines support; other platforms return 501. - one of: facebook, instagram, tiktok, linkedin, pinterest, google, twitter, openai
- **name** (required) `string`: Nonempty list name, trimmed before use.
### Responses
#### 200: Successful response.
**Response Body:**
- **updated** `boolean`: No description
- **customerId** `string`: Resolved Google Ads customer id.
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access and permission to the selected account are required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: Ambiguous campaign or account selection. Use a profile-scoped key. A list still attached to a campaign may also be rejected by Google. The account may also be inactive or need reconnection (code ads_connection_required). Reconnect it and read GET /v1/accounts for its current ID before retrying.
#### 422: Google Ads connection is missing or unavailable.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Available only on Google Ads.
---
## DELETE /v1/ads/accounts/negative-keyword-lists/{listId}
**Delete a negative keyword list**
Removes the Google shared negative keyword list. Detach it from all campaigns first; an in-use list is rejected. Only NEGATIVE_KEYWORDS shared sets are supported.
### Parameters
- **listId** (required) in path: No description
- **accountId** (required) in query: No description
- **customerId** (optional) in query: No description
- **platform** (optional) in query: No description
### Responses
#### 200: Successful response.
**Response Body:**
- **removed** `boolean`: No description
- **customerId** `string`: Resolved Google Ads customer id.
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 403: Ads access and permission to the selected account are required.
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: Ambiguous campaign or account selection. Use a profile-scoped key. A list still attached to a campaign may also be rejected by Google. The account may also be inactive or need reconnection (code ads_connection_required). Reconnect it and read GET /v1/accounts for its current ID before retrying.
#### 422: Google Ads connection is missing or unavailable.
#### 429: Google Ads operations budget or platform quota exhausted.
#### 501: Available only on Google Ads.
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Replace a value rule set API Reference
**THIS IS A FULL REPLACE, NOT A PATCH.** Meta's update is declarative: the body you
send becomes the rule set.
- `GET /v1/ads/value-rule-sets/{valueRuleSetId}` FIRST.
- Keep a rule or criterion by echoing its `id`.
- Create one by including the object WITHOUT an `id`.
- Delete one by OMITTING it from the array. There is no warning and no undo.
`name` and `rules` are both required for exactly this reason: a partial body would
silently destroy every rule left out.
**Rule order is semantic**: the array order you send is the evaluation order, and only
the first matching rule adjusts the bid for an overlapping audience.
Existing rule sets created elsewhere may contain `LOCATION_DMA` criteria. Those went
inert on 2026-06-22 and are rejected here; migrate them to `LOCATION_COMSCORE_MARKET`.
## GET /v1/ads/value-rule-sets/{valueRuleSetId}
**Read a value rule set**
Reads one value rule set including every nested rule id and criterion id. This is step
one of any edit: `PUT` is a full replace, so you need the ids before you can keep the
objects you are not changing.
Meta's own read returns `GENDER` values lowercase (`"male"`) while writes require
`"MALE"`. Values are passed through untouched, so never case-compare a stored rule
against a fetched one.
### Parameters
- **valueRuleSetId** (required) in path: Platform value rule set id.
- **accountId** (required) in query: Zernio SocialAccount id (posting or ads variant) used to resolve the Meta token.
### Responses
#### 200: Value rule set
**Response Body:**
- **valueRuleSet**: `ValueRuleSet` - See schema definition
#### 400: Invalid input, or Meta rejected the read. A bad id comes back as GraphMethodException code 100 / subcode 33, which cannot be told apart from a permission problem.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 501: Only supported on Meta (facebook/instagram)
---
## PUT /v1/ads/value-rule-sets/{valueRuleSetId}
**Replace a value rule set**
**THIS IS A FULL REPLACE, NOT A PATCH.** Meta's update is declarative: the body you
send becomes the rule set.
- `GET /v1/ads/value-rule-sets/{valueRuleSetId}` FIRST.
- Keep a rule or criterion by echoing its `id`.
- Create one by including the object WITHOUT an `id`.
- Delete one by OMITTING it from the array. There is no warning and no undo.
`name` and `rules` are both required for exactly this reason: a partial body would
silently destroy every rule left out.
**Rule order is semantic**: the array order you send is the evaluation order, and only
the first matching rule adjusts the bid for an overlapping audience.
Existing rule sets created elsewhere may contain `LOCATION_DMA` criteria. Those went
inert on 2026-06-22 and are rejected here; migrate them to `LOCATION_COMSCORE_MARKET`.
### Parameters
- **valueRuleSetId** (required) in path: Platform value rule set id.
### Request Body
- **accountId** (required) `string`: Zernio SocialAccount id (posting or ads variant); its platform decides where the campaign is created.
- **name** (required) `string`: Required: the update replaces the whole set.
- **rules** (required) `array`: The COMPLETE rule list. Omitting a rule deletes it on Meta.
### Responses
#### 200: Value rule set replaced
**Response Body:**
- **valueRuleSetId** `string`: No description
- **name** `string`: No description
- **rules** `array[ValueRule]`:
- **message** `string`: No description
#### 400: Invalid input, or Meta rejected the update
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 501: Only supported on Meta (facebook/instagram)
---
## DELETE /v1/ads/value-rule-sets/{valueRuleSetId}
**Delete a value rule set**
Deletes the rule set (Meta's `POST /{value-rule-set-id}/delete_rule_set`, a custom
action edge rather than an HTTP DELETE on its side). Ad sets pointing at it are not
modified here; detach them first with `valueRulesApplied: false` on
`PUT /v1/ads/ad-sets/{adSetId}`.
### Parameters
- **valueRuleSetId** (required) in path: Platform value rule set id.
- **accountId** (required) in query: Zernio SocialAccount id (posting or ads variant) used to resolve the Meta token.
### Responses
#### 200: Value rule set deleted
**Response Body:**
- **valueRuleSetId** `string`: No description
- **message** `string`: No description
#### 400: Invalid input, or Meta rejected the delete. A bad id comes back as GraphMethodException code 100 / subcode 33, which reads like a permission error rather than a 404.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 501: Only supported on Meta (facebook/instagram)
---
# Related Schema Definitions
## ValueRuleSet
A named set of bid-adjustment rules on an ad account. Attach it to an ad set with
`valueRuleSetId`. Limits: 6 sets per ad account, 10 rules per set, 4 criteria per rule.
### Properties
- **id** (required) `string`: Platform value rule set id.
- **name** (required) `string`: No description
- **rules** (required) `array`: Evaluated in order; the first matching rule wins.
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
## ValueRule
One bid-adjustment rule. Rules are evaluated in ARRAY ORDER and only the first matching
rule adjusts the bid for an overlapping audience, so the order is semantic.
### Properties
- **id** `string`: Platform rule id. Echo it on `PUT` to KEEP this rule, omit it to CREATE a new one.
A rule left out of the array entirely is DELETED.
- **name** (required) `string`: No description (max: 255)
- **adjustSign** (required) `string`: Direction of the adjustment. There is no signed value field. - one of: INCREASE, DECREASE
- **adjustValue** (required) `integer`: Unsigned percentage magnitude. `INCREASE` accepts 1-1000, `DECREASE` accepts 1-90.
0 is out of range on both.
(min: 1) (max: 1000)
- **status** `string`: Meta returns `ACTIVE` here but documents no enum for the field. Treat it as a
passthrough: echo whatever the `GET` returned, and do not synthesize values.
- **criteria** (required) `array`: All criteria on a rule must match for the rule to fire.
---
# Create a standalone creative API Reference
Creates a creative in the library WITHOUT an ad, reusable on the create endpoints via
`existingCreativeId`. Provide exactly one of `imageUrl` (uploaded server-side),
`imageHash` (from POST /v1/ads/images or the library list), or `carouselCards` (2-10
hand-built cards). The Page (and linked Instagram account, when present) is resolved
from `accountId` as the story actor. `creativeFeatures` configures Advantage+
enhancements. `promotion` is not supported and any object is rejected with 400.
## GET /v1/ads/creatives
**Creative library**
Lists the ad account's creative library (Meta's `/act_X/adcreatives`), rows returned
verbatim. The default projection covers id, name, status, object type, thumbnail,
object_story_spec / asset_feed_spec and url_tags; `fields` is a raw-passthrough
override. Any creative id here is reusable on the create endpoints via
`existingCreativeId`.
### Parameters
- **accountId** (required) in query: Zernio SocialAccount id (posting or ads variant) used to resolve the Meta token.
- **adAccountId** (required) in query: Meta ad account id (act_).
- **fields** (optional) in query: Comma-separated Graph field override. Supports nested {} projections and Graph field modifiers, so a nested edge can be paged explicitly: without a .limit() modifier the expansion runs at the Meta default page size and the tail is dropped silently.
- **limit** (optional) in query: Rows per page
- **after** (optional) in query: Cursor from paging.after of the previous page.
### Responses
#### 200: Creatives (raw Meta shape)
**Response Body:**
- **adAccountId** `string`: No description
- **data** `array[object]`:
Type: `object`
- **paging** `object`:
- **after** `string,null`: Cursor for the next page; null when exhausted.
#### 400: Invalid input, or Meta rejected the query
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 501: Only supported on Meta (facebook/instagram)
---
## POST /v1/ads/creatives
**Create a standalone creative**
Creates a creative in the library WITHOUT an ad, reusable on the create endpoints via
`existingCreativeId`. Provide exactly one of `imageUrl` (uploaded server-side),
`imageHash` (from POST /v1/ads/images or the library list), or `carouselCards` (2-10
hand-built cards). The Page (and linked Instagram account, when present) is resolved
from `accountId` as the story actor. `creativeFeatures` configures Advantage+
enhancements. `promotion` is not supported and any object is rejected with 400.
### Request Body
- **accountId** (required) `string`: Zernio SocialAccount id (posting or ads variant) used to resolve the Meta token and Page.
- **adAccountId** (required) `string`: Platform ad account id (Meta act_, Google customer id, LinkedIn account id, ...).
- **headline** (required) `string`: No description
- **body** (required) `string`: Primary text
- **description** `string`: Link description below the headline; omitted = Meta scrapes the destination's OG description.
- **callToAction** `string`: CTA type (same whitelist as POST /v1/ads/create).
- **linkUrl** (required) `string`: No description
- **imageUrl** `string`: Publicly reachable image; uploaded to the account's library server-side.
- **imageHash** `string`: Existing library image hash (POST /v1/ads/images or GET /v1/ads/images).
- **carouselCards** `array`: No description
- **urlTags** `string`: Appended to every outbound URL (e.g. utm_source=fb).
- **promotion**: Not supported. Meta validates creative_sourcing_spec.promotion_metadata_spec on the create call and then discards it, so a Promotion set through the Marketing API never reaches the creative. Any object is rejected with 400 invalid_field_value. Send null or omit the field, and set the Promotion on the ad in Ads Manager. Verified on 2026-09-11 across Graph v19.0 to v25.0 and every write path.
- **creativeFeatures**: Meta only. Applied to each new creative, including standalone and attach shapes. With creatives[], these are defaults; an item replaces the whole feature map, including an empty map. auto_promotion_tag is an Advantage+ enhancement, not the Ads Manager Promotion setting.
- **multiAdvertiser** `string`: Meta only. Multi-advertiser ads: whether Meta may show this ad alongside other advertisers' in one unit. Meta auto-enrols since Aug 2024, so send OPT_OUT to leave. It is a top-level creative field, NOT a `creativeFeatures` key, and Meta rejects it there. - one of: OPT_IN, OPT_OUT
### Responses
#### 201: Creative created
**Response Body:**
- **adAccountId** `string`: No description
- **creativeId** `string`: Platform creative id, reusable via existingCreativeId.
#### 400: Invalid input, or Meta rejected the create
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 422: No Facebook Page found to act as the story actor
#### 501: Only supported on Meta (facebook/instagram)
#### 502: Meta accepted the request then failed to produce the media (upload session, chunk transfer, processing timeout, or a response with no image hash). Inspect `platformError.reason`.
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Delete a creative API Reference
Deletes a creative from the library. Meta only allows deleting creatives not referenced
by any ad; otherwise its 400 surfaces verbatim.
## GET /v1/ads/creatives/{creativeId}
**Creative details**
One creative's details, verbatim from Meta. `fields` is a raw-passthrough override of
the default projection.
### Parameters
- **creativeId** (required) in path: Platform creative id
- **accountId** (required) in query: Zernio SocialAccount id (posting or ads variant) used to resolve the Meta token.
- **fields** (optional) in query: Comma-separated Graph field override. Supports nested {} projections and Graph field modifiers, so a nested edge can be paged explicitly: without a .limit() modifier the expansion runs at the Meta default page size and the tail is dropped silently.
### Responses
#### 200: Creative details
**Response Body:**
- **creative** `object`: Raw Meta creative node
#### 400: Invalid input, or Meta rejected the query
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 501: Only supported on Meta (facebook/instagram)
---
## PUT /v1/ads/creatives/{creativeId}
**Rename a creative**
Renames a creative. Creatives are immutable on Meta beyond `name`. For content changes
create a new creative (POST /v1/ads/creatives) and swap it onto the ad
(PUT /v1/ads/{adId} with `creative`).
### Parameters
- **creativeId** (required) in path: Platform creative id
### Request Body
- **accountId** (required) `string`: Zernio SocialAccount id (posting or ads variant); its platform decides where the campaign is created.
- **name** (required) `string`: No description
### Responses
#### 200: Creative renamed
**Response Body:**
- **creativeId** `string`: No description
- **name** `string`: No description
- **message** `string`: No description
#### 400: Invalid input, or Meta rejected the update
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 501: Only supported on Meta (facebook/instagram)
---
## DELETE /v1/ads/creatives/{creativeId}
**Delete a creative**
Deletes a creative from the library. Meta only allows deleting creatives not referenced
by any ad; otherwise its 400 surfaces verbatim.
### Parameters
- **creativeId** (required) in path: Platform creative id
- **accountId** (required) in query: Zernio SocialAccount id (posting or ads variant) used to resolve the Meta token.
### Responses
#### 200: Creative deleted
**Response Body:**
- **creativeId** `string`: No description
- **message** `string`: No description
#### 400: Invalid input, the creative is in use, or Meta rejected the delete
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 501: Only supported on Meta (facebook/instagram)
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Delete an ad video API Reference
Removes a video from the ad account's video library. Meta's canonical
`DELETE /{video_id}` fails with code 10 / subcode 1363055 for videos uploaded via
`/act_X/advideos` even with `ads_management`; this endpoint uses the working
account-scoped shape `DELETE /act_X/advideos?video_id=` and returns Meta's
`{success: true}` verbatim. Deleting a video that lives in a different ad account,
or that Meta has already removed, returns Meta's error verbatim as a 4xx.
## DELETE /v1/ads/videos/{videoId}
**Delete an ad video**
Removes a video from the ad account's video library. Meta's canonical
`DELETE /{video_id}` fails with code 10 / subcode 1363055 for videos uploaded via
`/act_X/advideos` even with `ads_management`; this endpoint uses the working
account-scoped shape `DELETE /act_X/advideos?video_id=` and returns Meta's
`{success: true}` verbatim. Deleting a video that lives in a different ad account,
or that Meta has already removed, returns Meta's error verbatim as a 4xx.
### Parameters
- **videoId** (required) in path: Meta ad video id (numeric).
- **accountId** (required) in query: Zernio SocialAccount id (posting or ads variant) used to resolve the Meta token.
- **adAccountId** (required) in query: Meta ad account id (act_) that owns the video.
### Responses
#### 200: Video deleted
**Response Body:**
- **adAccountId** `string`: No description
- **videoId** `string`: No description
- **success** `boolean`: No description
#### 400: Invalid request
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 401: Unauthorized
**Response Body:**
- **error** `string`: No description (example: "Unauthorized")
#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.
**Response Body:**
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
#### 501: Only supported on Meta (facebook/instagram)
---
# Related Schema Definitions
## ErrorResponse
Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).
### Properties
- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.
- **details** `object`: Additional structured context (e.g. field-level validation errors), for example `privateReplyConsumed` on the private-reply endpoint's 400 when the comment's single reply is already spent.
---
# Render pre-create ad previews API Reference
Renders how a creative would look per placement BEFORE any ad exists, via Meta's
`/generatepreviews`. Provide exactly one creative source: `existingCreativeId` or `creativeSpec`.
Each preview is an HTML `