Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.2.0
info:
title: Everpop Campaigns API
version: 1.3.1
description: 'Turn your own videos into published Shorts for YouTube, Instagram, Facebook and TikTok, with measured receipts on eligible YouTube Shorts. Built to be driven by AI agents: upload a file, clip it, publish it, and pull real 48h/7d performance — all by API. REST and MCP need the Pro or Scale plan; other accounts get HTTP 402 with reason subscription (no active subscription) or plan (the plan doesn''t include API access). Full agent guide: https://everpop.app/skill.md · MCP connector: https://everpop.app/api/mcp/mcp (OAuth, or Authorization: Bearer epk_...)'
termsOfService: https://everpop.app/terms
x-changelog:
- version: 1.3.1
date: '2026-10-16'
changes: 'Changed: when no natural sentence end fits a clip, it can now end on a held final frame, flagged open_ending in qcWarnings (one whose hold could not land is flagged mid_sentence_end); both wait for review and never auto-post. A render whose clips all end that way hands its video back, so GET /api/v1/usage does not count it. A video gets one uncharged render a month, shared with repeat-only renders, whichever came first.'
- version: 1.3.0
date: '2026-10-08'
changes: 'Changed: POST /api/v1/clip on a video whose strong moments were all clipped already completed FAILED with nothing new to clip; it now completes with one clip that reuses the best of them, flagged repeat_moment in qcWarnings. The first such render of a video in a month hands its video back, so GET /api/v1/usage does not count it. Automatic renders keep the old answer.'
- version: 1.2.0
date: '2026-10-01'
changes: 'Changed: the upload size limit was 4 GB and is now 32 GB. POST /api/v1/videos/upload answered 413 file_too_large for a file over 4 GB; it now takes a file up to 5 GB, the most one PUT can carry. A file over 5 GB and up to 32 GB answers 413 file_too_large with reason single_put and browserUploadUrl: the user uploads it in the browser. A file over 32 GB still answers 413 file_too_large. Added: browserUploadUrl on that error body.'
- version: 1.1.0
date: '2026-09-24'
changes: 'Changed: a 401 answered code unauthorized and now names its case: key_missing, key_invalid or key_expired. The Instagram posting-safety cap answered 429 rate_limited and now answers posting_cap. A 429 sent Retry-After: 60 whatever its cause; it now fits the cause (the seconds until resetsAt for quota_exhausted, 180 for queue_capacity, none for upload_cleanup_backlog, upload_retention_backlog and posting_cap, which carry retryable: false). A method a route does not support answered an empty 405; it now answers 405 method_not_allowed with an Allow header. Added: error bodies document their detail fields and a list of known codes (ErrorCode), including meta_publish_frozen. GET /api/v1/posts takes clipId. generateShort is marked deprecated while paused. Examples on the upload, finalize, clip, job, publish and receipts operations.'
- version: 1.0.0
date: '2026-07-03'
changes: First published spec.
contact:
email: support@everpop.app
url: https://everpop.app/contact
servers:
- url: https://everpop.app
security:
- apiKey: []
tags:
- name: Campaigns
paths:
/api/v1/campaigns:
get:
operationId: listCampaigns
summary: List campaigns with post counts, measured views and the public payout-report…
responses:
'200':
description: The account's campaigns with their post rollup and public report link.
content:
application/json:
schema:
$ref: '#/components/schemas/CampaignListEnvelope'
'402':
description: reason=plan — campaigns are not included in this account's plan.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'429':
$ref: '#/components/responses/RateLimited'
tags:
- Campaigns
post:
operationId: createCampaign
summary: Create a paid-clipping campaign (Pro plan or above)
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- name
properties:
name:
type: string
minLength: 2
maxLength: 120
sourceVideoId:
type: string
minLength: 1
maxLength: 64
description: The sponsor's footage in the user's library; posts published from its clips attach automatically.
sourceUrl:
type: string
format: uri
maxLength: 500
description: Link to the brief (stored, never fetched).
cpmCents:
type: integer
minimum: 0
maximum: 100000
budgetCents:
type: integer
minimum: 0
maximum: 1000000000
deadline:
type: string
format: date-time
notes:
type: string
maxLength: 2000
brief:
type: object
description: Structured brief rules enforced on every attach.
responses:
'200':
description: The created campaign.
content:
application/json:
schema:
$ref: '#/components/schemas/CampaignEnvelope'
'402':
description: Subscription required, or reason=plan.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'404':
description: sourceVideoId is not in the user's library.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'422':
description: The body did not validate, the brief could not be read, or both brief and notes were sent.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'429':
$ref: '#/components/responses/RateLimited'
tags:
- Campaigns
/api/v1/campaigns/{id}/attach:
post:
operationId: attachToCampaign
summary: Attach the user's published posts to a campaign (a post counts toward one…
parameters:
- name: id
in: path
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- postIds
properties:
postIds:
type: array
minItems: 1
maxItems: 100
items:
type: string
minLength: 1
description: postId values from GET /api/v1/receipts or the campaign proof.
responses:
'200':
description: How many posts were attached.
content:
application/json:
schema:
$ref: '#/components/schemas/AttachEnvelope'
'403':
description: A post id is not one of the user's published posts (named in the message).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'404':
description: Campaign not found for this account.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'422':
description: A post is not published yet, a post breaks the campaign's brief rules or the brief cannot be read, or the body did not validate.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'401':
$ref: '#/components/responses/Unauthorized'
'402':
$ref: '#/components/responses/PaymentRequired'
'429':
$ref: '#/components/responses/RateLimited'
tags:
- Campaigns
/api/v1/campaigns/{id}/proof:
get:
operationId: campaignProof
summary: 'A campaign''s measured payout report: per-clip 48h/7d numbers, totals, estimated…'
parameters:
- name: id
in: path
required: true
schema:
type: string
- name: csv
in: query
required: false
schema:
type: string
description: Any value returns a CSV download instead of JSON.
responses:
'200':
description: The payout report as JSON, or with ?csv a CSV download (one row per post).
content:
application/json:
schema:
$ref: '#/components/schemas/CampaignProofEnvelope'
text/csv:
schema:
type: string
'401':
$ref: '#/components/responses/Unauthorized'
'402':
$ref: '#/components/responses/PaymentRequired'
'403':
$ref: '#/components/responses/Forbidden'
'404':
description: Campaign not found for this account.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'429':
$ref: '#/components/responses/RateLimited'
tags:
- Campaigns
components:
schemas:
CampaignEnvelope:
type: object
properties:
ok:
const: true
data:
type: object
properties:
campaign:
$ref: '#/components/schemas/Campaign'
required:
- campaign
required:
- ok
- data
ErrorCode:
type: string
description: 'Codes an agent branches on, each explained in https://everpop.app/skill.md (Error codes). The list grows additively: handle a code that is not listed by its HTTP status.'
enum:
- bad_request
- unauthorized
- key_missing
- key_invalid
- key_expired
- payment_required
- subscription
- plan
- forbidden
- scope
- suspended
- disabled
- content_review
- not_found
- method_not_allowed
- conflict
- idempotency_conflict
- clip_removed
- upload_not_ready
- file_too_large
- unprocessable
- validation_failed
- invalid_cursor
- upload_required
- generated_source
- demo_source
- unknown_destination
- no_eligible_destination
- clip_not_ready
- rights_unconfirmed
- rate_limited
- quota_exhausted
- queue_capacity
- upload_slots
- upload_cleanup_backlog
- upload_retention_backlog
- posting_cap
- internal_error
- upstream_error
- unavailable
- engine_offline
- meta_publish_frozen
- cleanup_pending
CampaignProofEnvelope:
type: object
properties:
ok:
const: true
data:
type: object
properties:
campaign:
type: object
properties:
id:
type: string
name:
type: string
sourceUrl:
type:
- string
- 'null'
sourceVideoId:
type:
- string
- 'null'
sourceVideoTitle:
type:
- string
- 'null'
platformRule:
type: string
cpmCents:
type:
- integer
- 'null'
budgetCents:
type:
- integer
- 'null'
deadline:
type:
- string
- 'null'
format: date-time
status:
type: string
enum:
- ACTIVE
- COMPLETED
required:
- id
- name
- sourceUrl
- sourceVideoId
- sourceVideoTitle
- platformRule
- cpmCents
- budgetCents
- deadline
- status
briefLines:
type: array
items:
type: string
description: The brief rules every attached post passed.
items:
type: array
items:
$ref: '#/components/schemas/ProofItem'
description: The campaign's published posts, newest first.
totals:
type: object
properties:
posts:
type: integer
views48h:
type: number
views7d:
type: number
verifiedViews:
type: number
description: 'Measured views: per post the 7-day window when measured, else 48h.'
required:
- posts
- views48h
- views7d
- verifiedViews
estimatedPayoutCents:
type:
- integer
- 'null'
description: totals.verifiedViews ÷ 1,000 × cpmCents, rounded; null without a CPM. An estimate — the marketplace's own view count governs payout.
bundleUrl:
type:
- string
- 'null'
format: uri
description: One token-protected link to the measured receipts; null when none is measured.
publicUrl:
type:
- string
- 'null'
format: uri
description: The token-protected public payout-report page.
required:
- campaign
- briefLines
- items
- totals
- estimatedPayoutCents
- bundleUrl
- publicUrl
required:
- ok
- data
ErrorEnvelope:
type: object
properties:
ok:
const: false
error:
type: string
description: Human-readable; safe to show the user.
reason:
type: string
description: 'Kept for compatibility where it existed (e.g. subscription, plan, scope, unknown_destination, content_review). With code=file_too_large, single_put: the file is over 5 GB, the most one PUT can carry, and within the 32 GB limit.'
code:
anyOf:
- $ref: '#/components/schemas/ErrorCode'
- type: string
description: 'Stable machine code, on every error: the specific code (e.g. upload_required, quota_exhausted, invalid_cursor, file_too_large), else the reason, else by status (bad_request, not_found, rate_limited, internal_error, …). Branch on this.'
retryable:
type: boolean
description: 'On some 429s. false (quota_exhausted, upload_cleanup_backlog, upload_retention_backlog, posting_cap): waiting a minute will not help; stop and tell the user. true (queue_capacity): retry after Retry-After.'
issues:
type: object
description: 'With code=validation_failed: the field errors ({ formErrors, fieldErrors }).'
used:
type: integer
description: 'With code=quota_exhausted: videos (or AI shorts) used in the current window.'
limit:
type: integer
description: 'With code=quota_exhausted: the plan''s monthly allowance.'
resetsAt:
type: string
format: date-time
description: 'With code=quota_exhausted: when the allowance resets.'
scope:
type: string
enum:
- videos
- generations
- user
- global
description: 'With code=quota_exhausted: which allowance (videos or generations). With code=queue_capacity: whose queue is full (user or global).'
destinationIds:
type: array
items:
type: string
description: 'With code=unknown_destination: the requested ids that are not connected and healthy.'
uploadUrl:
type: string
format: uri
description: 'With code=upload_required: the sign-in-gated page where the user uploads this video''s file.'
browserUploadUrl:
type: string
format: uri
description: 'With code=file_too_large and reason=single_put: the sign-in-gated page where the user uploads the file (up to 32 GB).'
required:
- ok
- error
- code
ProofItem:
type: object
properties:
postId:
type: string
title:
type:
- string
- 'null'
publishedUrl:
type:
- string
- 'null'
postedAt:
type:
- string
- 'null'
format: date-time
outcomes:
type: object
properties:
h48:
oneOf:
- $ref: '#/components/schemas/ProofWindow'
- type: 'null'
d7:
oneOf:
- $ref: '#/components/schemas/ProofWindow'
- type: 'null'
required:
- h48
- d7
platform:
type: string
enum:
- TIKTOK
- INSTAGRAM_REELS
- YOUTUBE_SHORTS
- FACEBOOK_REELS
- THREADS
description: Only YouTube Shorts are measured; posts on other platforms carry no outcomes.
learnExclude:
type: boolean
description: 'true when the post is excluded from learning: by the creator, or automatically when the Short went private or was deleted on YouTube, or because it is a Weekly Pop. Outcome capture skips it, so it gets no new measured windows.'
receiptUrl:
type:
- string
- 'null'
format: uri
required:
- postId
- title
- publishedUrl
- postedAt
- outcomes
- platform
- learnExclude
- receiptUrl
CampaignListItem:
type: object
description: Every Campaign field, plus the rollup of the campaign's published posts.
properties:
id:
type: string
userId:
type: string
name:
type: string
sourceUrl:
type:
- string
- 'null'
sourceVideoId:
type:
- string
- 'null'
platformRule:
type: string
cpmCents:
type:
- integer
- 'null'
budgetCents:
type:
- integer
- 'null'
deadline:
type:
- string
- 'null'
format: date-time
notes:
type:
- string
- 'null'
status:
type: string
enum:
- ACTIVE
- COMPLETED
createdAt:
type: string
format: date-time
updatedAt:
type: string
format: date-time
postCount:
type: integer
description: Published posts attached to the campaign.
verifiedViews:
type: number
description: 'Measured YouTube Analytics views summed over the attached posts: per post the 7-day window when measured, else 48h (so a lower bound while 48h counts still climb).'
measuredCount:
type: integer
description: Attached posts with at least one measured window.
platformCounts:
type: object
additionalProperties:
type: integer
description: Attached posts per platform.
estimatedPayoutCents:
type:
- integer
- 'null'
description: verifiedViews ÷ 1,000 × cpmCents, rounded; null without a CPM. An estimate — the marketplace's own view count governs payout.
publicUrl:
type:
- string
- 'null'
format: uri
description: The token-protected public payout-report page.
source:
oneOf:
- type: object
properties:
id:
type: string
title:
type: string
status:
type: string
rightsOk:
type: boolean
description: Whether publishing from this footage is allowed (the user's rights attestation is on file).
required:
- id
- title
- status
- rightsOk
- type: 'null'
brief:
type:
- object
- 'null'
description: The structured brief rules enforced on every attach, or null.
briefLines:
type: array
items:
type: string
description: The brief rules as plain sentences.
briefUnreadable:
type: boolean
description: true when stored brief rules could not be read (attaching refuses until they are fixed).
required:
- id
- userId
- name
- sourceUrl
- sourceVideoId
- platformRule
- cpmCents
- budgetCents
- deadline
- notes
- status
- createdAt
- updatedAt
- postCount
- verifiedViews
- measuredCount
- platformCounts
- estimatedPayoutCents
- publicUrl
- source
- brief
- briefLines
- briefUnreadable
PaywallError:
type: object
properties:
ok:
const: false
error:
type: string
reason:
type: string
enum:
- subscription
- plan
code:
type: string
enum:
- subscription
- plan
description: The same value as reason.
required:
- ok
- error
- reason
- code
ProofWindow:
type: object
properties:
views:
type:
- number
- 'null'
averageViewPercentage:
type:
- number
- 'null'
likes:
type:
- number
- 'null'
subscribersGained:
type:
- number
- 'null'
measuredAt:
type: string
format: date-time
required:
- views
- averageViewPercentage
- likes
- subscribersGained
- measuredAt
CampaignListEnvelope:
type: object
properties:
ok:
const: true
data:
type: object
properties:
campaigns:
type: array
items:
$ref: '#/components/schemas/CampaignListItem'
description: Newest first.
required:
- campaigns
required:
- ok
- data
AttachEnvelope:
type: object
properties:
ok:
const: true
data:
type: object
properties:
attached:
type: integer
description: Distinct posts attached (repeated ids count once).
required:
- attached
required:
- ok
- data
Campaign:
type: object
description: A campaign row as stored.
properties:
id:
type: string
description: The id /api/v1/campaigns/{id}/attach and /proof take.
userId:
type: string
description: The owner (the calling account).
name:
type: string
sourceUrl:
type:
- string
- 'null'
description: Link to the sponsor's brief — stored, never fetched.
sourceVideoId:
type:
- string
- 'null'
description: The sponsor's footage in the user's library; posts published from its clips attach automatically.
platformRule:
type: string
cpmCents:
type:
- integer
- 'null'
budgetCents:
type:
- integer
- 'null'
deadline:
type:
- string
- 'null'
format: date-time
notes:
type:
- string
- 'null'
description: Free-text notes, or the stored form of structured brief rules (read brief / briefLines on GET /api/v1/campaigns instead).
status:
type: string
enum:
- ACTIVE
- COMPLETED
createdAt:
type: string
format: date-time
updatedAt:
type: string
format: date-time
required:
- id
- userId
- name
- sourceUrl
- sourceVideoId
- platformRule
- cpmCents
- budgetCents
- deadline
- notes
- status
- createdAt
- updatedAt
responses:
Forbidden:
description: 'code=scope (also reason=scope): the API key is read-only and this is a write. code=suspended: the account is suspended. Endpoint-specific refusals document their own reason.'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
PaymentRequired:
description: 'REST and MCP need the Pro or Scale plan. reason=subscription: the account has no active subscription (subscribe, or redeem an invite code). reason=plan: the account''s plan doesn''t include this (API and agent access need Pro or Scale).'
content:
application/json:
schema:
$ref: '#/components/schemas/PaywallError'
Unauthorized:
description: 'The API key did not authenticate. code=key_missing: no Authorization: Bearer epk_... header. code=key_invalid: the key is unknown or revoked. code=key_expired: the key passed its expiry; the user creates a new one in Dashboard → API.'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
RateLimited:
description: 'Too many requests. code=rate_limited: the per-minute limit; wait Retry-After (60) seconds. code=quota_exhausted (with used, limit, resetsAt, retryable: false): the monthly allowance is used up; Retry-After is the seconds until resetsAt — tell the user rather than wait. code=queue_capacity (retryable: true): the render queue is full; retry after Retry-After (180).'
headers:
Retry-After:
$ref: '#/components/headers/RetryAfter'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
headers:
RetryAfter:
description: 'Seconds to wait before retrying: 60 for a per-minute rate limit (rate_limited, upload_slots), 180 for queue_capacity, the seconds until resetsAt for quota_exhausted. Absent on upload_cleanup_backlog, upload_retention_backlog and posting_cap, where waiting will not help soon.'
schema:
type: integer
securitySchemes:
apiKey:
type: http
scheme: bearer
bearerFormat: epk_...
description: Per-user API key from everpop.app/dashboard/api (Dashboard → API). Write scope required for mutating endpoints.