Developer resources

Build with the Partner API

Submit merchant deals, follow their progress, receive signed webhooks and pull residuals from your own software.

View OpenAPI specOpenAPI 1.1.0
Base URL
https://your-portal-host/api/partner/v1

JSON requests and responses use camelCase. Money amounts are integer cents in USD. Each key belongs to one partner account.

Quickstart

  1. Create a test key in the partner portal under Developer API. Save the full key when shown. Don't see Developer API? Ask your administrator to turn on API access for your account.
  2. Confirm your key with GET /me:
curl "https://your-portal-host/api/partner/v1/me" \
  -H "Authorization: Bearer ptk_test_YOUR_KEY"

Submit a deal with a unique idempotency key, then simulate a status change using the returned deal id:

curl -X POST "https://your-portal-host/api/partner/v1/deals" \
  -H "Authorization: Bearer ptk_test_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: your-deal-id-123" \
  -d '{"contactName":"Jordan Lee","companyName":"Lee Coffee Bar"}'

curl -X POST "https://your-portal-host/api/partner/v1/sandbox/deals/DEAL_ID/simulate" \
  -H "Authorization: Bearer ptk_test_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status":"approved"}'

Authentication

Send Authorization: Bearer ptk_live_... for live requests or Authorization: Bearer ptk_test_... for test requests. Keep keys server-side and never commit or expose them in a browser.

Every response includes X-Request-Id for troubleshooting. During key rotation grace, X-Api-Key-Grace-Ends gives the grace deadline.

bearerAuth · http bearer

Authorization: Bearer ptk_live_... (live) or ptk_test_... (test). Create keys in the partner portal under Developer API. The full key is shown once.

Test mode

Test keys access sandbox data only: they never email sales or create real deals. Use /sandbox/deals/{id}/simulate to change a test deal status and /sandbox/residuals/simulate to trigger a test residuals webhook. Live keys cannot use sandbox endpoints.

Idempotency

Send an Idempotency-Key header on deal creation. A retry with the same key and body returns the original response with Idempotent-Replayed: true; reusing a key with a different body returns 409 idempotency_conflict.

Pagination

Pass limit and optional cursor when listing deals. Read hasMore and pass the returned nextCursor as the next request's cursor until no more results remain.

Rate limits

Each key is limited to 120 requests per minute. A 429 rate_limited response includes Retry-After (seconds to wait) and X-RateLimit-Limit headers.

Errors

Failures use the shape {"error":{"code":"...","message":"...","details":{}}}; details is optional. Handle these codes:

unauthorizedforbiddennot_foundvalidation_erroridempotency_conflictrate_limitedtest_mode_onlyinsufficient_permissiondeal_lockedinternal_error

Deal statuses

Stable public deal status. received: submitted, not yet worked. in_progress: being worked by the sales team. application_submitted: merchant application submitted for underwriting. approved: application approved. boarding: merchant account being set up. live: processing. declined: application declined. closed: closed without an account (e.g. merchant not interested).

receivedin_progressapplication_submittedapprovedboardinglivedeclinedclosed

Webhooks

Configure your endpoint and signing secret (whsec_...) in the partner portal. Events are delivered at least once; deduplicate by X-Webhook-Id. X-Webhook-Event identifies the event type.

Verify X-Webhook-Signature in the form t=<unix>,v1=<hex>. The v1 value is the hex HMAC-SHA256 of `${t}.${rawBody}` using your endpoint signing secret. Use the exact raw request body, reject timestamps where |now − t| exceeds 300 seconds, and compare signatures in constant time.

Return any 2xx within 10 seconds. Failed deliveries are retried with exponential backoff, up to 8 attempts.

Events

deal.created

A deal credited to you was created.

{
  "id": "evt_example",
  "object": "event",
  "type": "deal.created",
  "createdAt": "2026-09-29T12:00:00Z",
  "livemode": false,
  "data": {
    "deal": {
      "id": 123,
      "contactName": "Jordan Lee",
      "companyName": "Lee Coffee Bar",
      "status": "received",
      "createdAt": "2026-09-29T12:00:00Z"
    }
  }
}
deal.status_changed

A deal's public status changed. Moves inside one public status never fire.

{
  "id": "evt_example",
  "object": "event",
  "type": "deal.status_changed",
  "createdAt": "2026-09-29T12:00:00Z",
  "livemode": false,
  "data": {
    "deal": {
      "id": 123,
      "status": "in_progress",
      "previousStatus": "received",
      "changedAt": "2026-09-29T12:00:00Z"
    }
  }
}
deal.approved

A deal moved to approved (also sends deal.status_changed).

{
  "id": "evt_example",
  "object": "event",
  "type": "deal.approved",
  "createdAt": "2026-09-29T12:00:00Z",
  "livemode": false,
  "data": {
    "deal": {
      "id": 123,
      "status": "approved",
      "previousStatus": "in_progress",
      "changedAt": "2026-09-29T12:00:00Z"
    }
  }
}
deal.declined

A deal was declined (also sends deal.status_changed).

{
  "id": "evt_example",
  "object": "event",
  "type": "deal.declined",
  "createdAt": "2026-09-29T12:00:00Z",
  "livemode": false,
  "data": {
    "deal": {
      "id": 123,
      "status": "declined",
      "previousStatus": "in_progress",
      "changedAt": "2026-09-29T12:00:00Z"
    }
  }
}
residuals.posted

Residuals for a month were posted or updated for you.

{
  "id": "evt_example",
  "object": "event",
  "type": "residuals.posted",
  "createdAt": "2026-09-29T12:00:00Z",
  "livemode": false,
  "data": {
    "residuals": {
      "month": "2026-09"
    }
  }
}
webhook.test

A test delivery to verify your endpoint.

{
  "id": "evt_example",
  "object": "event",
  "type": "webhook.test",
  "createdAt": "2026-09-29T12:00:00Z",
  "livemode": false,
  "data": {
    "test": true,
    "message": "This is a test event from the partner portal.",
    "endpointId": 42
  }
}

Verify in Node.js

const { createHmac, timingSafeEqual } = require("node:crypto");

function verifyWebhook(rawBody, signature, secret) {
  const match = /^t=(\d+),v1=([a-f0-9]{64})$/.exec(signature || "");
  if (!match) return false;
  const [, t, hex] = match;
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const expected = createHmac("sha256", secret)
    .update(t + ".").update(rawBody).digest();
  const received = Buffer.from(hex, "hex");
  return received.length === expected.length &&
    timingSafeEqual(received, expected);
}
// Pass the unparsed request body (Buffer) and your whsec_... secret.

Verify in Python

import hmac
import hashlib
import time


def verify_webhook(raw_body: bytes, signature: str, secret: str) -> bool:
    try:
        parts = dict(part.split("=", 1) for part in signature.split(","))
        timestamp, received = parts["t"], parts["v1"]
        if abs(time.time() - int(timestamp)) > 300:
            return False
        expected = hmac.new(
            secret.encode(), timestamp.encode() + b"." + raw_body,
            hashlib.sha256
        ).hexdigest()
        return hmac.compare_digest(expected, received)
    except (ValueError, KeyError):
        return False

Permissions & key lifecycle

read_only keys can only use GET endpoints. read_write keys can also create or change resources. Writing with a read-only key returns 403 insufficient_permission.

Keys can have an optional expiry. An expired key returns 401 unauthorized. Rotating a key issues a new secret; the previous key remains usable until its rotation grace deadline (up to seven days). After grace, the old key is refused. Revoke a key to disable it immediately.

Integration endpoints

Recover missed webhooks with GET /events (filter by type, createdAfter and cursor) and GET /events/{id}. Events are returned newest first.

Update a submitted deal with PATCH /deals/{id} while it is received or in progress; withdraw with POST /deals/{id}/withdraw and an optional reason. Once locked, updates return 409 deal_locked.

Use GET /merchants to list your live merchants with anonymized processor names. Each merchant carries dealId, the deal it was boarded from (null when it is not linked to one of your deals).

API reference

Endpoints below are relative to the base URL. Examples and response definitions come from the published OpenAPI specification.

GET/openapi.jsonSpec

Download this API specification

Responses

200 This document.

application/json

{
  "type": "object"
}
GET/meAccount

Identify the partner and key

Handy first call to confirm your key works and whether it is live or test.

Responses

200 The partner behind this key.

application/json

{
  "object": "partner",
  "livemode": false,
  "partner": {
    "id": 731,
    "name": "Acme Referral Partners"
  },
  "apiKey": {
    "id": 12,
    "mode": "test"
  }
}

401 Missing, malformed, revoked, expired or unknown key, or a rotated key past its grace period (unauthorized).

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

403 The partner account behind the key is not active (forbidden), or a read-only key called a write endpoint (insufficient_permission).

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

429 Too many requests for this key (rate_limited). Wait for Retry-After seconds.

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

500 Unexpected server error (internal_error). Safe to retry; use an Idempotency-Key for writes.

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}
POST/dealsDeals

Submit a deal

Creates a merchant lead credited to your partner account, exactly like a portal submission. Only contactName is required; unknown fields are ignored. Send an Idempotency-Key header so a retried request never creates a duplicate: repeating the same key with the same body returns the original response (with Idempotent-Replayed: true); the same key with a different body returns 409 idempotency_conflict. Test keys create sandbox deals only.

Parameters

NameInRequiredDetails
Idempotency-KeyheaderNoUnique value per logical deal, e.g. your own record id.

Request body · required

application/json

{
  "contactName": "Jordan Lee",
  "companyName": "Lee's Coffee Bar",
  "contactEmail": "jordan@leescoffee.example",
  "contactPhone": "+13055550142",
  "monthlyVolume": "45000",
  "averageTicket": "12.50",
  "businessCity": "Miami",
  "businessState": "FL",
  "notes": "Wants countertop terminal"
}

Responses

201 Deal created (or replayed).

application/json

{
  "id": 48213,
  "object": "deal",
  "livemode": true,
  "status": "received",
  "contactName": "Jordan Lee",
  "companyName": "Lee's Coffee Bar",
  "contactEmail": "jordan@leescoffee.example",
  "contactPhone": "+13055550142",
  "monthlyVolume": "45000",
  "createdAt": "2026-09-01T14:03:11.000Z",
  "updatedAt": "2026-09-03T09:12:40.000Z"
}

400 Validation failed (validation_error).

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

401 Missing, malformed, revoked, expired or unknown key, or a rotated key past its grace period (unauthorized).

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

403 The partner account behind the key is not active (forbidden), or a read-only key called a write endpoint (insufficient_permission).

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

409 Idempotency-Key reused with a different body (idempotency_conflict).

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

429 Too many requests for this key (rate_limited). Wait for Retry-After seconds.

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

500 Unexpected server error (internal_error). Safe to retry; use an Idempotency-Key for writes.

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}
GET/dealsDeals

List your deals

Newest first. Page with cursor = the previous page's nextCursor.

Parameters

NameInRequiredDetails
limitqueryNo{"type":"integer","minimum":1,"maximum":100,"default":25}
cursorqueryNoReturn deals older than this id.
statusqueryNo{"$ref":"#/components/schemas/DealStatus"}

Responses

200 A page of deals.

application/json

{
  "object": "list",
  "livemode": true,
  "data": [
    {
      "id": 48213,
      "object": "deal",
      "livemode": true,
      "status": "in_progress",
      "contactName": "Jordan Lee",
      "companyName": "Lee's Coffee Bar",
      "contactEmail": "jordan@leescoffee.example",
      "contactPhone": "+13055550142",
      "monthlyVolume": "45000",
      "createdAt": "2026-09-01T14:03:11.000Z",
      "updatedAt": "2026-09-03T09:12:40.000Z"
    }
  ],
  "hasMore": false,
  "nextCursor": null
}

400 Invalid query parameter.

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

401 Missing, malformed, revoked, expired or unknown key, or a rotated key past its grace period (unauthorized).

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

403 The partner account behind the key is not active (forbidden), or a read-only key called a write endpoint (insufficient_permission).

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

429 Too many requests for this key (rate_limited). Wait for Retry-After seconds.

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

500 Unexpected server error (internal_error). Safe to retry; use an Idempotency-Key for writes.

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}
GET/deals/{id}Deals

Get one deal with its status history

Parameters

NameInRequiredDetails
idpathYesDeal id.

Responses

200 The deal.

application/json

{
  "id": 48213,
  "object": "deal",
  "livemode": true,
  "status": "in_progress",
  "contactName": "Jordan Lee",
  "companyName": "Lee's Coffee Bar",
  "contactEmail": "jordan@leescoffee.example",
  "contactPhone": "+13055550142",
  "monthlyVolume": "45000",
  "createdAt": "2026-09-01T14:03:11.000Z",
  "updatedAt": "2026-09-03T09:12:40.000Z",
  "history": [
    {
      "status": "received",
      "at": "2026-09-01T14:03:11.000Z"
    },
    {
      "status": "in_progress",
      "at": "2026-09-02T16:20:05.000Z"
    }
  ]
}

401 Missing, malformed, revoked, expired or unknown key, or a rotated key past its grace period (unauthorized).

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

403 The partner account behind the key is not active (forbidden), or a read-only key called a write endpoint (insufficient_permission).

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

404 No such deal for this key (not_found). Deals belonging to anyone else also return 404.

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

429 Too many requests for this key (rate_limited). Wait for Retry-After seconds.

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

500 Unexpected server error (internal_error). Safe to retry; use an Idempotency-Key for writes.

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}
PATCH/deals/{id}Deals

Update a deal

Updates fields on a deal you submitted while its status is received or in_progress. Once the deal reaches application_submitted (underwriting) or later, returns 409 deal_locked. Requires a read-write key. Safe to retry.

Parameters

NameInRequiredDetails
idpathYesDeal id.

Request body · required

application/json

{
  "contactPhone": "+13055550199",
  "monthlyVolume": "52000"
}

Responses

200 The updated deal.

application/json

{
  "id": 48213,
  "object": "deal",
  "livemode": true,
  "status": "in_progress",
  "contactName": "Jordan Lee",
  "companyName": "Lee's Coffee Bar",
  "contactEmail": "jordan@leescoffee.example",
  "contactPhone": "+13055550199",
  "monthlyVolume": "52000",
  "createdAt": "2026-09-01T14:03:11.000Z",
  "updatedAt": "2026-09-03T09:12:40.000Z"
}

400 Invalid input (validation_error).

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

401 Missing, malformed, revoked, expired or unknown key, or a rotated key past its grace period (unauthorized).

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

403 The partner account behind the key is not active (forbidden), or a read-only key called a write endpoint (insufficient_permission).

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

404 No such resource for this key (not_found). Resources belonging to anyone else also return 404.

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

409 The deal is in underwriting or later and can no longer be changed through the API (deal_locked). details.status holds its current status.

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

429 Too many requests for this key (rate_limited). Wait for Retry-After seconds.

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

500 Unexpected server error (internal_error). Safe to retry; use an Idempotency-Key for writes.

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}
POST/deals/{id}/withdrawDeals

Withdraw a deal

Withdraws a deal you submitted while it is received or in_progress; the deal moves to closed and a deal.status_changed event fires. Idempotent: withdrawing an already-withdrawn deal returns it unchanged. Returns 409 deal_locked once the deal is in underwriting or later. Requires a read-write key.

Parameters

NameInRequiredDetails
idpathYesDeal id.

Request body · optional

application/json

{
  "reason": "Merchant signed elsewhere"
}

Responses

200 The withdrawn deal.

application/json

{
  "id": 48213,
  "object": "deal",
  "livemode": true,
  "status": "closed",
  "contactName": "Jordan Lee",
  "companyName": "Lee's Coffee Bar",
  "contactEmail": "jordan@leescoffee.example",
  "contactPhone": "+13055550142",
  "monthlyVolume": "45000",
  "createdAt": "2026-09-01T14:03:11.000Z",
  "updatedAt": "2026-09-03T09:12:40.000Z"
}

400 Invalid input (validation_error).

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

401 Missing, malformed, revoked, expired or unknown key, or a rotated key past its grace period (unauthorized).

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

403 The partner account behind the key is not active (forbidden), or a read-only key called a write endpoint (insufficient_permission).

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

404 No such resource for this key (not_found). Resources belonging to anyone else also return 404.

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

409 The deal is in underwriting or later and can no longer be changed through the API (deal_locked). details.status holds its current status.

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

429 Too many requests for this key (rate_limited). Wait for Retry-After seconds.

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

500 Unexpected server error (internal_error). Safe to retry; use an Idempotency-Key for writes.

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}
GET/merchantsMerchants

List live merchants

Your merchants that are live, newest first. The processor is an anonymized lane label; real processor names are never returned. Test keys list sandbox deals simulated to live.

Parameters

NameInRequiredDetails
limitqueryNo{"type":"integer","minimum":1,"maximum":100,"default":25}
cursorqueryNonextCursor from the previous page.

Responses

200 A page of merchants.

application/json

{
  "object": "list",
  "livemode": true,
  "data": [
    {
      "object": "merchant",
      "id": 912,
      "livemode": true,
      "businessName": "Lee's Coffee Bar",
      "dba": "Lee's Coffee",
      "mccCode": "5814",
      "status": "live",
      "processor": "Lane 2",
      "liveSince": "2026-09-20"
    }
  ],
  "hasMore": false,
  "nextCursor": null
}

400 Invalid input (validation_error).

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

401 Missing, malformed, revoked, expired or unknown key, or a rotated key past its grace period (unauthorized).

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

403 The partner account behind the key is not active (forbidden), or a read-only key called a write endpoint (insufficient_permission).

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

429 Too many requests for this key (rate_limited). Wait for Retry-After seconds.

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

500 Unexpected server error (internal_error). Safe to retry; use an Idempotency-Key for writes.

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}
GET/eventsEvents

List events

Events for this key's partner and mode, newest first. Use it to recover events your webhook endpoint missed. Events are kept for 90 days and are recorded while you hold an API key of that mode.

Parameters

NameInRequiredDetails
typequeryNo{"$ref":"#/components/schemas/EventType"}
createdAfterqueryNoOnly events created after this time.
limitqueryNo{"type":"integer","minimum":1,"maximum":100,"default":25}
cursorqueryNonextCursor from the previous page.

Responses

200 A page of events.

application/json

{
  "object": "list",
  "livemode": true,
  "data": [
    {
      "object": "event",
      "id": "evt_3f9a0c1e8b7d4a2f9e6c5b4a3d2e1f00",
      "type": "deal.status_changed",
      "createdAt": "2026-09-02T16:20:05.000Z",
      "livemode": true,
      "data": {
        "deal": {
          "id": 48213,
          "status": "in_progress",
          "previousStatus": "received",
          "changedAt": "2026-09-02T16:20:05.000Z"
        }
      }
    }
  ],
  "hasMore": false,
  "nextCursor": null
}

400 Invalid input (validation_error).

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

401 Missing, malformed, revoked, expired or unknown key, or a rotated key past its grace period (unauthorized).

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

403 The partner account behind the key is not active (forbidden), or a read-only key called a write endpoint (insufficient_permission).

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

429 Too many requests for this key (rate_limited). Wait for Retry-After seconds.

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

500 Unexpected server error (internal_error). Safe to retry; use an Idempotency-Key for writes.

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}
GET/events/{id}Events

Get one event

Parameters

NameInRequiredDetails
idpathYesEvent id (evt_...), the same id sent in the webhook body.

Responses

200 The event.

application/json

{
  "object": "event",
  "id": "evt_3f9a0c1e8b7d4a2f9e6c5b4a3d2e1f00",
  "type": "deal.status_changed",
  "createdAt": "2026-09-02T16:20:05.000Z",
  "livemode": true,
  "data": {
    "deal": {
      "id": 48213,
      "status": "in_progress",
      "previousStatus": "received",
      "changedAt": "2026-09-02T16:20:05.000Z"
    }
  }
}

401 Missing, malformed, revoked, expired or unknown key, or a rotated key past its grace period (unauthorized).

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

403 The partner account behind the key is not active (forbidden), or a read-only key called a write endpoint (insufficient_permission).

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

404 No such resource for this key (not_found). Resources belonging to anyone else also return 404.

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

429 Too many requests for this key (rate_limited). Wait for Retry-After seconds.

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

500 Unexpected server error (internal_error). Safe to retry; use an Idempotency-Key for writes.

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}
GET/residualsResiduals

Residuals by month

Monthly totals and per-merchant lines for your account — the same figures as the Earnings view in your partner portal. Defaults to the last 12 months; at most 24 months per request.

Parameters

NameInRequiredDetails
fromqueryNoExample: 2026-01
toqueryNoExample: 2026-08

Responses

200 Residual report.

application/json

{
  "object": "residual_report",
  "livemode": true,
  "currency": "USD",
  "from": "2026-07",
  "to": "2026-08",
  "totals": {
    "volumeCents": 9120000,
    "earningsCents": 22800,
    "lineCount": 2
  },
  "months": [
    {
      "month": "2026-08",
      "volumeCents": 4600000,
      "earningsCents": 11500,
      "lines": [
        {
          "merchantId": 9001,
          "merchantName": "Lee's Coffee Bar",
          "type": "merchant",
          "volumeCents": 4600000,
          "earningsCents": 11500
        }
      ]
    },
    {
      "month": "2026-07",
      "volumeCents": 4520000,
      "earningsCents": 11300,
      "lines": [
        {
          "merchantId": 9001,
          "merchantName": "Lee's Coffee Bar",
          "type": "merchant",
          "volumeCents": 4520000,
          "earningsCents": 11300
        }
      ]
    }
  ]
}

400 Invalid month range.

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

401 Missing, malformed, revoked, expired or unknown key, or a rotated key past its grace period (unauthorized).

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

403 The partner account behind the key is not active (forbidden), or a read-only key called a write endpoint (insufficient_permission).

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

429 Too many requests for this key (rate_limited). Wait for Retry-After seconds.

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

500 Unexpected server error (internal_error). Safe to retry; use an Idempotency-Key for writes.

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}
GET/residuals/{month}Residuals

Residuals for one month

Parameters

NameInRequiredDetails
monthpathYesExample: 2026-08

Responses

200 One month.

application/json

{
  "object": "residual_month",
  "livemode": true,
  "currency": "USD",
  "month": "2026-08",
  "volumeCents": 4600000,
  "earningsCents": 11500,
  "lines": [
    {
      "merchantId": 9001,
      "merchantName": "Lee's Coffee Bar",
      "type": "merchant",
      "volumeCents": 4600000,
      "earningsCents": 11500
    }
  ]
}

400 Invalid month.

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

401 Missing, malformed, revoked, expired or unknown key, or a rotated key past its grace period (unauthorized).

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

403 The partner account behind the key is not active (forbidden), or a read-only key called a write endpoint (insufficient_permission).

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

429 Too many requests for this key (rate_limited). Wait for Retry-After seconds.

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

500 Unexpected server error (internal_error). Safe to retry; use an Idempotency-Key for writes.

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}
POST/sandbox/deals/{id}/simulateSandbox

Move a sandbox deal to a status

Test keys only. Changes a sandbox deal's status and fires the same webhooks a real change would (deal.status_changed, plus deal.approved/deal.declined).

Parameters

NameInRequiredDetails
idpathYesDeal id.

Request body · required

application/json

{
  "status": "approved"
}

Responses

200 The updated sandbox deal.

application/json

{
  "$ref": "#/components/schemas/DealWithHistory"
}

400 Invalid status.

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

401 Missing, malformed, revoked, expired or unknown key, or a rotated key past its grace period (unauthorized).

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

403 Live key used (test_mode_only).

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

404 No such sandbox deal.

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

429 Too many requests for this key (rate_limited). Wait for Retry-After seconds.

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

500 Unexpected server error (internal_error). Safe to retry; use an Idempotency-Key for writes.

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}
POST/sandbox/residuals/simulateSandbox

Fire a residuals.posted webhook

Test keys only. Sends a test-mode residuals.posted event to your test webhook endpoints.

Request body · optional

application/json

{
  "month": "2026-08"
}

Responses

202 Event queued.

application/json

{
  "$ref": "#/components/schemas/SimulatedEvent"
}

400 Invalid month.

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

401 Missing, malformed, revoked, expired or unknown key, or a rotated key past its grace period (unauthorized).

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

403 Live key used (test_mode_only).

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

429 Too many requests for this key (rate_limited). Wait for Retry-After seconds.

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

500 Unexpected server error (internal_error). Safe to retry; use an Idempotency-Key for writes.

application/json · error response

{
  "$ref": "#/components/schemas/ErrorResponse"
}

Changelog

2026-09-29 · Developer API expansion

Events retrieval, merchant listing, deal updates and withdrawals, key permissions and expiry, rotation grace, request IDs and expanded webhook management.

Initial v1 release

Partner API launched with deal submission, residuals, sandbox testing and signed webhooks.