ViralPayouts API

Run a two-tier partner program, or your own partner account, from code. The REST API at https://viralpayouts.com/api/v1 has 66 endpoints: 45 for brands and 21 for partners. The same keys work with the MCP server for AI agents.

45
brand endpoints (vpk_ keys)
21
partner endpoints (vpp_ keys)
43
MCP tools for a brand key
21
MCP tools for a partner key

Quickstart

Create a key, then call the API with it as a bearer token. Every answer is JSON. Money is in minor units (cents) with a currency.

List your programs
curl https://viralpayouts.com/api/v1/programs \
  -H "Authorization: Bearer vpk_..."
Answer
{
  "data": [ { "id": "prg_9f2c1a", "name": "Acme Pro", "status": "live", "commission": "25% of each sale", ... } ],
  "has_more": false,
  "next_cursor": null
}

Every endpoint, with its fields and an example answer, is in the API reference. The machine-readable contract is the OpenAPI 3.1 document at /api/v1/openapi.json: import it into Postman, an SDK generator or an agent.

Authentication and keys

Send a key in the Authorization header: Authorization: Bearer <key>. A key is shown once, when it is created. ViralPayouts keeps only a SHA-256 of it and its last four characters. A revoked key stops working on the next request.

KeyWho creates it, and whereWhat it can do
vpk_...A brand owner, in the brand console: Integration tab, API keys.Acts in one brand with one role, exactly like a team member with that role: viewer reads everything; manager also reviews partners and conversions, sends messages and test sales; owner also creates and changes programs, sets rates and webhook secrets, records refunds and reads the signing secret.
vpp_...A partner, in the partner portal: Your API keys. Only from a session opened by a one-time sign-in link sent to their email address.Acts for one person across every program they are in. read sees programs, links, stats, conversions, payouts, earnings, recruits and the marketplace, and builds links. write also joins programs, sets postback URLs and sends messages. No key can connect or change a payout account, save a tax form or delete the account.

A brand key on a partner endpoint answers 403 partner_key_required, and a partner key on a brand endpoint 403 brand_key_required. A record of another brand answers 404, exactly like one that does not exist.

Errors

Every error has the same shape: the HTTP status, a stable code, a message saying what happened, and next, the next step (which field to fix, which endpoint to call, which key to use). A server error also carries a requestId to quote to us.

HTTP/1.1 403 Forbidden
{
  "error": {
    "status": 403,
    "code": "role_too_low",
    "message": "This key has the viewer role, and POST /api/v1/programs needs an owner key.",
    "next": "Ask a brand owner to create a key with the owner role in the console (Integration tab, API keys), then call again with it."
  }
}
StatusCodesWhen
400invalid_request invalid_json invalid_cursor idempotency_key_required invalid_idempotency_keyA field is missing, of the wrong type, out of range or unknown. The message names it.
401missing_key invalid_keyNo key, or a key that is mistyped, revoked, or from another network.
403role_too_low scope_too_low brand_key_required partner_key_requiredThe key cannot use this endpoint.
404no_such_endpoint program_not_found partner_not_found conversion_not_found payout_not_found batch_not_found program_not_joined program_not_listedNo such endpoint, or no such record for this key.
405method_not_allowedThe path exists but not with this method. The Allow header lists the methods.
409conflict idempotency_in_progress commission_payableThe change does not apply to the record as it is now (nothing was changed), or an idempotent request is still running.
413too_largeThe body is over 100 KB.
422idempotency_key_reusedAn Idempotency-Key was reused for a different request.
429rate_limitedToo many requests. Wait for Retry-After seconds.
500server_errorOur fault. Nothing was changed. Retry; quote the requestId if it persists.

Pagination

Lists are newest first and use cursors. Send limit (1 to 100, default 25). The answer has data, has_more and next_cursor. To get the next page, send cursor=<next_cursor> with the same filters. A cursor marks the last row you saw, so new rows arriving between pages never make you see a row twice or miss one.

curl "https://viralpayouts.com/api/v1/programs/prg_9f2c1a/conversions?status=pending&limit=50&cursor=MjAyNi0xMC..." \
  -H "Authorization: Bearer vpk_..."

Rate limits

Each key may make 120 requests a minute. Every answer carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset (seconds until the window ends). Past the limit the answer is 429 with Retry-After. Requests with keys that are not valid are limited too: 30 a minute from one address. The MCP endpoint has its own limit of 120 requests a minute per key.

Idempotency

Send an Idempotency-Key header (1 to 255 visible characters, a UUID is ideal) on a POST, PUT or PATCH to make a retry safe. The first answer is kept for 24 hours. The same key with the same request gets that answer again, with Idempotent-Replayed: true, and nothing runs twice. The same key with a different request gets 422 idempotency_key_reused. A retry while the first request is still running gets 409 idempotency_in_progress. A server error (5xx) is not kept, so you can retry it with the same key.

Recording a refund moves money, so POST /conversions/{conversionId}/refund requires the header. Over MCP the same value is the idempotencyKey argument.

curl -X POST https://viralpayouts.com/api/v1/conversions/cnv_0c88aa/refund \
  -H "Authorization: Bearer vpk_..." \
  -H "Idempotency-Key: 6f1c2f0e-3b7a-4c1e-9a55-2d0f7f1b8e21" \
  -H "Content-Type: application/json" \
  -d '{"amountCents": 2500}'

Event API: send sales from your server

Your backend reports each sale, refund and app event to POST https://viralpayouts.com/api/v1/events, signed with the program's signing secret (an owner key reads it from GET /programs/{programId}/integration, or copy it from the console's Integration tab). This endpoint uses the signature, not an API key.

HeaderValue
x-vp-programThe program id (or slug).
x-vp-timestampThe time you signed, in unix seconds. It must be within 5 minutes of our clock.
x-vp-signatureHex HMAC-SHA256 of <timestamp>.<raw body> with the signing secret. After you rotate the secret, the previous one keeps working for its grace period; the answer says which one matched (signedWith).
Node.js
import crypto from 'node:crypto';

const raw = JSON.stringify({ id: 'evt_1', type: 'sale', order_id: 'ord_1', amount_cents: 10000, currency: 'USD', click_id: clickId });
const ts = String(Math.floor(Date.now() / 1000));
const sig = crypto.createHmac('sha256', process.env.VP_SIGNING_SECRET).update(`${ts}.${raw}`).digest('hex');
await fetch('https://viralpayouts.com/api/v1/events', {
  method: 'POST',
  headers: { 'content-type': 'application/json', 'x-vp-program': 'prg_9f2c1a', 'x-vp-timestamp': ts, 'x-vp-signature': sig },
  body: raw,
});
FieldOnWhat it is
idevery eventRequired. Your unique id for this event, up to 200 characters. Sending the same id again is recorded once (the answer says duplicate), so retries are safe.
typeevery eventRequired. sale, refund or event (an app event such as a signup).
order_idsale, refundRequired. Your order id, up to 200 characters. A refund names the order it refunds.
amount_centssale, refundA sale: the amount in cents, a positive whole number. A refund: the amount of this refund, in cents.
refunded_total_centsrefundInstead of amount_cents: the total refunded on the order so far.
currencysaleThree-letter code, such as USD.
click_idsale, eventThe vp_click value from the landing URL. The surest way to credit the right partner.
ref_codesale, eventThe ref value from the landing URL (a partner handle or link code), when there is no click id.
customer_emailsaleUsed to check for self-referral, then stored only as a keyed hash and a masked form.
occurred_atsaleISO 8601 time of the sale. A time in the future is recorded as now.
sub_id (or sub1), sub2 to sub5saleThe partner's own labels. Each falls back to the one on the click.
coupon_codesaleA partner's coupon code the buyer used. Credits that partner while the program's coupon offer is on.
subscription_idsaleYour id for the subscription. Send it on every renewal: a renewal is credited to the partner who brought the first payment, for as long as the program's renewal period lasts.
product_id (or plan)saleThe product or plan sold. Paid that product's rate if the program sets one, otherwise the program's commission.
eventeventThe event name, such as signup or install. Paid the program's bounty for that name.
event_ideventYour id for what happened (the user, the install). The same event for the same id earns once.

The answer is { "status": "processed" | "duplicate" | "unmatched" | "error", "note", "conversionId", "signedWith" }: 200, or 422 when the status is error. unmatched means no partner could be credited (the note says why); a refund for an order not seen yet is kept and applied if the sale arrives. A body may be up to 64 KB. Each program may send 600 valid events a minute and each address may fail 20 times a minute by default; past either the answer is 429 with Retry-After.

Webhooks and postbacks

Sales coming in. Your checkout reports sales and refunds to ViralPayouts in one of three ways. GET /programs/{programId}/webhooks shows the URLs for a program and whether each secret is set; GET /programs/{programId}/events shows every event received and what happened to it.

SourceURLVerified by
Your backendPOST https://viralpayouts.com/api/v1/eventsThe signing secret, as above (Event API).
Stripehttps://viralpayouts.com/api/stripe/webhook/{programId}The Stripe endpoint's whsec_ secret, set with PUT /programs/{programId}/webhooks/stripe. Write-only.
Shopifyhttps://viralpayouts.com/api/shopify/webhook/{programId}The Shopify webhook secret, set with PUT /programs/{programId}/webhooks/shopify. Write-only.

Postbacks going out. A partner can give each program a postback URL (PUT /partner/programs/{program}/postback). When one of their sales is recorded, approved or reversed, ViralPayouts calls it once with a GET (5-second timeout, redirects not followed). A timeout, a 5xx, 408 or 429 is retried up to 6 times; another 4xx is not retried. Test sales never send one. Macros: {sub_id} {sub2} {sub3} {sub4} {sub5} {click_id} {conversion_id} {payout} {currency} {status} {event} {program}.

What the API covers

AreaBrand keyPartner key
ProgramsList, get, create, update, results, integration, funding and invoicesPrograms you are in; marketplace search; join
PartnersList, get, approve, reject, suspend, reinstate, warn, payment schedule, statsYour account and memberships
Links and clicksPartner links, clicks, sub-ID, country and device reportsYour links, links with sub IDs, your sub-ID, country and device reports
ConversionsList, get, approve, reject, refund, test salesYour sales and your recruits' sales
CommissionsThe ledger, balances per partnerYour balance per program and across programs
Two-tierRecruiter overrides earned, recruits, product rates and event bountiesYour recruits and the override you earned
PayoutsPayouts and payout batches (read)Your payouts
Messages and webhooksMessage a partner or all, webhook secrets, incoming eventsMessage the brand, postback URL and test

Starting payout batches, connecting payout accounts and tax forms stay in the console and the portal. The full list is in the reference.