REST API reference

Server-to-server API for managing referrals: list and inspect them, create them from your own systems, and report conversions. All endpoints are JSON over HTTPS.

Authentication

Every request must carry your API key in the X-API-Key header. Keys look like oft_ followed by 32 hex characters and are issued in your partner dashboard.

curl
curl https://offertown.net/api/v1/partners/referrals \
  -H "X-API-Key: oft_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  • The key identifies your partner account — there is no partnerId parameter anywhere in the API, and supplying one is rejected.
  • Keep the key server-side only. If it leaks, rotate it from the dashboard.
  • Missing or invalid keys return 401; keys for partners that aren’t approved yet return 403.

Rate limits

  • Write requests are limited to 30 per minute per IP.
  • Conversion reporting is additionally limited to 60 per minute per partner.
  • Exceeding a limit returns 429 with a Retry-After header and retryAfterMs in the body. Back off and retry — all write endpoints are idempotent, so retries are safe.

Errors

All errors share one shape:

Error response
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Human-readable description",
    "fields": [{ "field": "friendEmail", "message": "Invalid email" }],
    "retryAfterMs": 12000
  }
}
HTTPCodeMeaning
400VALIDATION_ERRORMalformed query parameter or body field.
401MISSING_API_KEY / INVALID_API_KEYNo key, or the key doesn’t resolve.
403PARTNER_NOT_APPROVEDValid key, but the partner account isn’t approved.
404NOT_FOUNDReferral doesn’t exist or isn’t yours.
409DUPLICATE_CONVERSIONThe referral was already converted under a different transaction ID.
429RATE_LIMITEDSlow down; retry after the indicated delay.
500INTERNAL_ERRORSomething went wrong on our side.

The referral object

Referral
{
  "referralId": "jx7…",            // stable ID, use it in URLs
  "referralCode": "a1b2c3d4",      // the referrer's 8-char share code
  "status": "pending",             // clicked | pending | converted | expired
  "friendEmail": "ana@example.com",
  "friendName": "Ana",
  "createdAt": 1765467600000,      // epoch ms
  "convertedAt": null,             // epoch ms when converted
  "conversionValue": null,         // decimal string of cents, e.g. "12999"
  "currency": null,                // "EUR" once converted
  "referrerIdentifier": "cust_42", // your identifier for the referrer
  "externalSourceId": null,        // your identifier for the referral
  "metadata": {}                   // free-form, up to 4 KiB
}

List referrals

GET/api/v1/partners/referrals
Query parameterDescription
statusclicked | pending | converted | expired
referrerEmailExact-match filter on the referrer’s email.
dateFrom / dateToCreation window, epoch milliseconds.
searchFree-text search (max 256 chars).
sortBy / sortOrdercreatedAt, convertedAt, or conversionValue; asc or desc.
page / pageSizePagination; default 1 / 25, max page size 100.
Response — 200
{
  "referrals": [ { …referral } ],
  "pagination": { "page": 1, "pageSize": 25, "total": 137 }
}

Create a referral

POST/api/v1/partners/referrals

Registers a referral you captured yourself (your own form, support flow, or import).

Request body
{
  "referrerIdentifier": "cust_42",     // required — how YOU identify the referrer
  "friendEmail": "ana@example.com",    // required
  "friendName": "Ana",                 // required
  "externalSourceId": "lead_991",      // optional — your reference
  "metadata": { "campaign": "spring" } // optional, ≤ 4 KiB
}
Response — 201 Created (200 if it already existed)
{
  "referralId": "jx7…",
  "referralCode": "a1b2c3d4",
  "status": "pending",
  "idempotent": false
}

Idempotency: the combination of referrerIdentifier + friendEmail is the idempotency key. Re-posting the same pair returns the existing referral with idempotent: true.

Get a referral

GET/api/v1/partners/referrals/{referralId}

Returns the full referral object.

Update a referral

PATCH/api/v1/partners/referrals/{referralId}

Only metadata and externalSourceIdare writable. Everything else — status, conversion data, the friend’s identity — is read-only and rejected if present in the body.

Request body
{
  "externalSourceId": "lead_991",
  "metadata": { "crmStage": "won" }
}

Report a conversion

POST/api/v1/partners/referrals/{referralId}/convert

Marks the referral converted and starts the reward flow (your approval, maturation, then funding).

Request body
{
  "conversionValue": 12999,            // optional — order value in cents
  "currency": "EUR",                   // optional — EUR only
  "externalTransactionId": "order_77", // strongly recommended — your order ID
  "metadata": { "sku": "PRO-1" }       // optional
}
Response — 201 Created (200 on idempotent replay)
{
  "referralId": "jx7…",
  "status": "converted",
  "convertedAt": 1765467600000,
  "conversionValue": "12999",
  "idempotent": false
}
  • Idempotency: replays with the same externalTransactionId return the original result. Reporting a conversion for an already-converted referral with a different transaction ID — or reusing a transaction ID on another referral — returns 409 DUPLICATE_CONVERSION.
  • Send conversionValue in cents (integer); responses echo it as a decimal string.

A typical integration

  1. Capture referred friends with the widget (or create referrals via POST /referrals from your backend).
  2. When an order completes in your system, look up the referral (by your externalSourceIdor the friend’s email via the list endpoint) and call …/convert with the order ID as externalTransactionId.
  3. Approve conversions in your partner dashboard; rewards mature and are funded automatically.