invitation.dev / docs / rest
partner REST API
A plain-HTTP projection of the same procedures the MCP tools call. The vocabulary follows the market — partners, commissions, programs, /track/sale — so an existing Rewardful, Tolt, FirstPromoter, or dub integration ports by changing the base URL and the key.
This surface is gated behind FEATURE_AGENT_API and returns 404 everywhere when it is off. Verify against your target deployment before wiring a production job to it.
# base url
https://api3.invitation.codes/api/partner/v1
Every response is JSON. Lists share one envelope — { data, has_more, next_cursor } — and every error shares one shape, { error: { code, message, doc_url } }. Switch on error.code, never on the message text.
# authentication
Bearer, or Basic if that is what you have
Mint a key in the business console under settings → API keys. Scopes are enforced per route: agent issues links, write records conversions, org:campaigns reads and manages a program. A key never gains permissions by coming in over REST rather than MCP.
curl https://api3.invitation.codes/api/partner/v1/me \
-H "Authorization: Bearer $INVITATION_API_KEY"# The API key may also ride as the HTTP Basic username,
# which is what Rewardful/PartnerStack integrations already send.
curl -u "$INVITATION_API_KEY:" https://api3.invitation.codes/api/partner/v1/me# reference
Endpoints
| method | path | what it does | scope |
|---|---|---|---|
| GET | /me | Key introspection — owner, scopes, org binding, earn totals. Start here. | any key |
| GET | /programs | Campaigns this key can administer. `program` is our campaign. | org:campaigns |
| GET | /partners | Referrers on a program. Filter by `status` and `email`. | org:campaigns |
| POST | /partners | Provision partners. Upserts on identity — safe to re-run. Batch via `partners: [...]`. | write |
| GET | /partners/:id | One referrer, by our id or `ext_<your-id>`. | org:campaigns |
| GET | /commissions | Conversions and what they earned. `voided` is accepted for `rejected`. | org:campaigns |
| POST | /links | Mint a tracked, disclosed referral link. Idempotent per key + program. | agent |
| GET | /links | Same primitive as a read, for clients that reach for GET first. | agent |
| POST | /track/lead | Record a non-revenue conversion (signup, trial, activation). | write |
| POST | /track/sale | Record a purchase. `revenue_cents` required. | write |
GET https://api3.invitation.codes/api/partner/v1 returns this list as JSON, so curl against the base URL is self-documenting rather than a 401.
OpenAPI 3.1
The API serves its own spec — point a client generator at it rather than hand-writing types. It needs no key, and servers[0].url reflects whichever deployment answered, so a spec fetched from staging targets staging.
curl https://api3.invitation.codes/api/partner/v1/openapi.json
# Generate a typed client from it, e.g.:
npx openapi-typescript https://api3.invitation.codes/api/partner/v1/openapi.json -o partner-api.d.ts# links
Mint a tracked link
Issuing a link enrolls the key as a referrer on that program the first time, then returns the same code on every later call — safe to retry. Pass sub_id to attribute per placement, thread, or channel.
curl -X POST https://api3.invitation.codes/api/partner/v1/links \
-H "Authorization: Bearer $INVITATION_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "program": "summer-launch", "sub_id": "thread-42" }'{
"url": "https://invitation.codes/campaigns/summer-launch?ref=agent-u_abc",
"referral_code": "agent-u_abc",
"enrollment_status": "approved",
"reward": { "referrer_coins": 500, "referee_coins": 250 },
"disclosure": "This is a referral link. The assistant's operator may earn a commission if you make a purchase — at no extra cost to you.",
"program": {
"slug": "summer-launch",
"name": "Summer launch",
"directory_url": "https://invitation.codes/campaigns/summer-launch"
}
}The disclosure is not optional
Every link response carries a `disclosure` string. Rendering it next to the link is a term of the agent program — FTC affiliate-disclosure rules apply to bot recommenders too. Repeated omission is grounds for key revocation. Agents that paste the whole tool result comply by default.
# partners
Provision your own customers
Turn customers you already have into referrers without asking them to create an Invitation account first. Each one gets a referral code and a participant URL you can drop into your own dashboard or an email.
# One partner. Re-posting the same identity returns the
# existing row with "created": false — safe to re-run.
curl -X POST https://api3.invitation.codes/api/partner/v1/partners \
-H "Authorization: Bearer $INVITATION_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"program": "summer-launch",
"external_id": "cus_1001",
"email": "alex@example.com"
}'
# Or a batch, in one round trip.
curl -X POST https://api3.invitation.codes/api/partner/v1/partners \
-H "Authorization: Bearer $INVITATION_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"program": "summer-launch",
"partners": [
{ "external_id": "cus_1001", "email": "alex@example.com" },
{ "email": "pri@example.com", "referral_code": "VIPPRI" }
]
}'Upsert, not insert
Identity is `email` or `external_id` — post the same one twice and you get the existing partner back with created: false rather than a 409. That makes this safe to call from a nightly sync that does not track what it has already sent.
# conversions
Record what happened
Capture session_token from the browser tracker on the click, then report the conversion from your server. /track/sale requires revenue_cents; /track/lead is for signups and other non-revenue events.
curl -X POST https://api3.invitation.codes/api/partner/v1/track/sale \
-H "Authorization: Bearer $INVITATION_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"session_token": "sess_abc",
"event_id": "order_1042",
"revenue_cents": 12900,
"currency": "USD",
"test_mode": true
}'curl -X POST https://api3.invitation.codes/api/partner/v1/track/lead \
-H "Authorization: Bearer $INVITATION_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"session_token": "sess_abc",
"event_id": "signup_884",
"customer_email": "buyer@example.com",
"test_mode": true
}'event_id is your idempotency key
It is required, and it must be stable across retries — reuse your own order or event id. A replay returns 409 duplicate_event rather than double-crediting, so a retrying job is safe by construction. Rewards credit on conversions only, never on clicks.
Rehearse with test_mode
Send test_mode: true to exercise the whole path — attribution, validation, response shape — without crediting rewards or firing merchant webhooks. Do this before pointing a production job at the endpoint.
# pagination
Cursors, not page numbers
List routes take limit (default 20, max 100) and starting_after. Treat next_cursor as opaque: pass back exactly what you were given. Stop when has_more is false.
# Page through partners. Cursors are opaque — pass back
# exactly what next_cursor returned, never construct one.
curl "https://api3.invitation.codes/api/partner/v1/partners?program=summer-launch&limit=50" \
-H "Authorization: Bearer $INVITATION_API_KEY"
# {
# "data": [ ... 50 rows ... ],
# "has_more": true,
# "next_cursor": "50"
# }# errors
One shape, always
{
"error": {
"code": "duplicate_event",
"message": "Duplicate eventId — conversion conv_1 already recorded.",
"doc_url": "https://invitation.dev/docs/conversions#idempotency"
}
}| status | code | when |
|---|---|---|
| 400 | invalid_request | A required field is missing or malformed. |
| 401 | unauthorized | No key, or the key is not recognised. |
| 403 | forbidden | The key is valid but lacks the scope, or is bound to another org. |
| 404 | not_found | No such resource — or the partner API is feature-gated off. |
| 409 | duplicate_event | This `event_id` was already recorded for the program. |
| 412 | precondition_failed | Not enrolled, or the program failed the agent safety policy. |
| 429 | rate_limited | Per-key rate limit exceeded. |
A 404 on every route usually means the surface is feature-gated off on that deployment, not that you got the path wrong — check FEATURE_AGENT_API before debugging your client.
# directory
Public reads need no key
The public catalog — programs, codes, users, shops, platform counts — lives on https://api3.invitation.codes/api/v2/* and is readable without credentials. @invitation/node covers it alongside the write path, so one package serves both; a key is optional and only raises rate limits.
import { InvitationNodeClient } from "@invitation/node";
// Public directory reads need no key — a key only raises rate limits.
const invitation = new InvitationNodeClient();
const featured = await invitation.programs.featured({ country: "US", limit: 5 });
const codes = await invitation.codes.listByProgram("notion");
const counts = await invitation.stats.publicCounts();