curl call and
the response shape straight from the API reference.
They all assume you already hold an access token. See the
OAuth quickstart for registration, the PKCE authorization-code flow, and
the token exchange. Every request goes to the production base URL and carries the bearer
token:
- Cursor pagination. List responses are
{ data, nextCursor, hasMore }. Pass an opaquecursor(from a priornextCursor) plus an optionallimit(default 25, max 100). WhenhasMoreisfalse,nextCursorisnull. - Idempotency. Every
POST/PATCH/PUTaccepts anIdempotency-Keyheader. Replaying the same key + body returns the original response; the same key with a different body is rejected with409 idempotency_conflict. Sensitive money-moving writes (/payouts/execute,/payouts/quick-pay) require it. - Errors. Every error is
{ error: { code, message, requestId } }. Lists and reads can return401/402/403/429; writes add400/404/409. - The token is bound to one company tenant - there is no cross-tenant
?scope=union, and a resource owned by another tenant is reported as404.
Dsc8SfHtPjzNGDKzMqBP, campaign
prog_abc123, contract ctr_abc123, creator user_xyz, post post_abc123,
invoice inv_abc123, conversation conv_abc123, webhook whk_abc123.
1. Create a complete campaign and update its payment structure
Goal: create a usable campaign in one call, update its CPM payment structure, then read it back. Scopes:campaigns:write to create/update, campaigns:read to read.
Create the campaign - POST /campaigns
The campaign must include complete payout/tracking terms. Tracking-only campaigns can omit payout
amounts and expected-post targets, but must set analyticsOnly: true and name at least one platform.
Omitted status defaults to active, so the campaign immediately appears in analytics filters; pass
"status": "draft" explicitly to keep it unpublished.
201 Created returns the new id:
Set the payment structure - PUT /campaigns/{id}/payment-structure
Replace the structure. The body wraps a paymentStructure object (forwarded to the
campaign update service verbatim - cpmRate >= 0 normalization and
crosspostMaturityDays validation are reused from the canonical service).
200 OK:
GET /campaigns/{id}/payment-structure:
Read the whole campaign - GET /campaigns/{id}
GET /campaigns lists campaigns (paged, with optional status and search filters).
PATCH /campaigns/{id} updates fields, and there are POST /campaigns/{id}/archive
and POST /campaigns/{id}/duplicate lifecycle actions, all campaigns:write.Optional next step - post the campaign to the marketplace
Theid returned above is the campaign’s programId. The final, opt-in step of
the create-campaign flow - mirroring the dashboard’s “Post & Get Applications”
button - is to post the campaign to the Creator Marketplace so creators can discover
it and apply. Pass that programId to POST /jobs (with marketplace targeting and a
cover image); see §8 Post an existing campaign to the marketplace.
Skip it if you’d rather bring creators on directly via invites or contract offers
(§3).
2. List and process applications
A campaign application is a creator’s handle request against one of your campaigns. The platform exposes approve and reject only - there is no “shortlist” action. Scopes:applications:read to list/get, applications:write to review.
List applications - GET /applications
Cursor-paginated, sorted pending → approved → rejected.
Application id is composite. The id is
{programId}_{requestId} (here
prog_abc123_r1) - that exact string is what you pass to the get and status
endpoints. Don’t try to reconstruct it from parts.Get one application - GET /applications/{id}
Approve - POST /applications/{id}/status
action is approve or reject only. Approving auto-accepts the program contract
and fires a Whop DM + signed PDF + notifications (sensitive), so use a fresh
Idempotency-Key.
200 OK - on approve you get the auto-created contract id and its status:
Reject - same endpoint, action: "reject"
3. Invite and contract a creator
Two ways to bring a creator onto a campaign: a shareable invite link (anyone with it can join), or a direct contract offer to a specific creator.Create a campaign invite link - POST /campaigns/{id}/invites
This is the product’s mechanism for inviting creators to a campaign. The body is
optional; you can cap uses and expiry. Returns a token + shareable link.
Scope: creators:write.
201 Created:
There is also a tenant-wide invite surface -
GET /invites (list, creators:read),
POST /invites (create with programId in the body, creators:write), and
DELETE /invites/{id} (revoke by token, creators:write). See workflow note
below. Team-member invites are not on this surface yet - a non-campaign
invite type is rejected with 400.Offer a contract - POST /contracts
The UI-equivalent of inviting a specific creator to a campaign. companyId is always
the token’s tenant (never client-supplied); programId must belong to it. Required:
programId, contractorId, contractorName. Fires the invite notification +
contract PDF (sensitive).
Scope: contracts:write.
201 Created:
List contracts - GET /contracts
Cursor-paginated, with optional status, programId, creatorId filters.
Scope: contracts:read.
GET /contracts/{id} returns a single contract in the same shape.
Update one creator’s contract - PATCH /contracts/{id}
Use the targeted update when one creator’s base retainer or posting platforms
change. Scope: contracts:write.
Only send the values you intend to change. The API resolves the contract’s
effective campaign + per-creator terms and preserves every unmentioned payment
field and requirement. platforms is the creator’s exact replacement set, not
a union. The campaign must already use per-creator payment terms; a
campaign-level contract returns 409 conflict with guidance to update the
campaign instead.
changed is false, and the
API does not regenerate the PDF, append history, or notify the creator. Sandbox
grants cannot call this endpoint because a real edit sends an external DM.
Cancel a contract - POST /contracts/{id}/cancel
Scope: contracts:write.
4. Add ghost handles and videos to a campaign
Goal: use the normal SideShift campaign flow to add tracked handles or videos, one at a time or in bulk. Scope:campaigns:write.
The campaign id lives in the URL and the company tenant comes from the OAuth token.
The bulk endpoints accept structured JSON, so API clients do not need to construct
the CSV used by the dashboard uploader.
Add one handle - POST /campaigns/{id}/ghost-handles
creatorId to attach the ghost handle to an existing creator. Supported
handle platforms are tiktok, instagram, youtube, snapchat, facebook,
twitter, and x.
Bulk add handles - POST /campaigns/{id}/ghost-handles/bulk
One request accepts 1–500 handles. Existing platform + handle pairs are skipped and
reported in the response.
Add one video - POST /campaigns/{id}/ghost-videos
creatorId and description are optional. Supported video platforms are tiktok,
instagram, youtube, and snapchat.
Bulk add videos - POST /campaigns/{id}/ghost-videos/bulk
One request accepts 1–500 videos. platform is optional when SideShift can detect it
from the URL; existing URLs in the campaign are skipped.
Backfill observed daily history - POST /campaigns/{id}/analytics-history
Use this when migrating from another analytics provider. Send cumulative observations (never
estimated/interpolated values) captured before SideShift’s first native snapshot. The endpoint
resolves each row within the token-bound company and campaign, derives daily deltas, re-bases the
first native snapshot, and verifies Firestore plus Postgres before an import succeeds.
The same endpoint has two modes:
validate(default) runs identity resolution and the complete splice plan without writes.importwrites the validated batch. It requires the exact currentconfirmCampaignName.
Idempotency-Key. Reuse a key only to retry the exact
same body. A batch supports 1–2,000 rows and at most 500 unique posts. Partition larger exports by
post; never split one post’s history across requests. Each included post must carry its complete
imported history so an earlier newly supplied date can safely recalculate successor deltas.
Validate first:
postId or postUrl is required; supplying both is allowed. Instagram shortcodes from provider
exports are resolved through the tracked URL. All five cumulative metrics are required because the
Firestore and Postgres history rows share that complete observation contract. Use 0 only when the
provider observed zero; do not substitute zero for an unavailable metric. Dates are UTC
YYYY-MM-DD. Cumulative declines are preserved as real negative deltas and returned as warnings.
When validation returns ready: true, submit the same rows in import mode with a fresh key:
*Truncated flags; summary counts always describe the full batch.
Unmatched rows block by default. After investigating a known unmatched subset, set
unmatchedPostPolicy: "skip"; an import must also provide expectedUnmatchedPostCount equal to the
validated batch’s exact summary.unmatchedPosts. This prevents a changed batch from silently
skipping more posts. Sandbox grants can validate, but cannot import.
Read analytics for tracked content
analytics:read, the response groups tracked posts by creator handle + platform
and includes per-account post, view, and engagement totals plus a cross-account
summary. The other aggregate reads are GET /analytics/overview, /analytics/kpis,
/analytics/time-series, and /analytics/videos.
5. Payout read flows
Read your wallet ledger, pending amounts, and balances. Scope:payouts:read.
Payout history - GET /payouts
Wallet ledger entries (payouts + deposits), cursor-paginated, with an aggregate
summary. Optional filters: type (debit/credit), creatorId, contractId,
status, fromDate, toDate, reason.
Pending payouts - GET /payouts/pending
Calculated pending/overdue amounts for active/expired contracts. Optional
programId, creatorId, minAmount, sortBy (amount/dueDate/creator),
sortOrder.
Wallet + payout stats - GET /payouts/stats
Money-moving writes exist but are out of scope for casual use.
POST /payouts/execute (run/dry-run contract payouts) and POST /payouts/quick-pay
(pay anyone by email) require payouts:write. They are sensitive: the
Idempotency-Key header is required (a 400 without it), and sandbox /
test-mode grants are rejected with 403 forbidden. Read the idempotency and
sandbox rules in Errors and rate limits before calling
them.6. Invoice lifecycle
Create, inspect, (re)send, and void an invoice. Scopes:invoices:write to create/send/void, invoices:read to list/get.
Create + send - POST /invoices
Creates and emails an invoice. Sandbox-rejected (fires real Stripe/Whop/Resend
side effects) - a sandbox grant returns 403 forbidden. Amounts are in cents.
201 Created (the created invoice plus a top-level url):
Get one - GET /invoices/{id}
(Re)send the email - POST /invoices/{id}/send
Sends a real email via Resend (sandbox-rejected). Returns the refreshed invoice.
Void - POST /invoices/{id}/void
Voids an unpaid invoice (cancels the Whop plan + any in-flight Stripe wire,
best-effort). Idempotent - voiding an already-void invoice returns its current state;
voiding a paid invoice is 409 conflict.
List - GET /invoices
Cursor-paginated; optional status (all/open/pending/partial_paid/paid/
void/overdue/refunded/partially_refunded) and customerEmail filters.
7. Messages
Read DM conversations and send messages as the company. Scopes:messages:read to list, messages:write to send.
List conversations - GET /conversations
Cursor-paginated. Read-only.
Read a conversation’s messages - GET /conversations/{id}/messages
Cursor-paginated. A conversation the tenant does not belong to is reported as 404.
Send a message - POST /messages
Sandbox-rejected - this fires a real Whop DM (best-effort) and persists to
Firestore, so a sandbox grant returns 403 forbidden. Provide exactly one of
conversationId (post into an existing thread) or creatorId (start/continue a
creator DM), plus content.
201 Created:
8. Post an existing campaign to the marketplace
Goal: post a campaign to the marketplace so creators can discover it and apply - the same result as the dashboard’s “Post & Get Applications” button. This is the opt-in final step of the create-campaign flow, whether the campaign is one you just created in §1 or an existing one. Scope:jobs:write.
A “campaign posted to the marketplace” is a job linked to the campaign. Create a
job with programId set to the campaign id: the job appears in the public feed and
its applications flow back to the campaign. (Creating a job spends 1 posting credit
and requires a cover image - pass a hosted imageUrl or an inline imageDataUrl.)
End to end, the flow is: create a campaign (§1)
→ (optional) post it to the marketplace with targeting (below) → applications
come in (§2).
Post the campaign - POST /jobs
201 Created returns the new job id - the campaign is now discoverable and
accepting applications:
Use
programIds instead of programId to link the job to several campaigns at
once. Applications land on the linked campaign(s) - process them with the
applications flow. To post to multiple
campaigns or pull the linked job later, GET /jobs lists the tenant’s jobs.The MCP equivalent is the create_job tool with programId set - same single
mechanism, same “Post & Get Applications” result.