Developer resources

Build with the Partner API

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

Live spec URL OpenAPI 1.0.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.
  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.

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_onlyinternal_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.

deal.status_changed

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

deal.approved

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

deal.declined

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

residuals.posted

Residuals for a month were posted or updated for you.

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

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 or unknown key (unauthorized).

application/json · error response

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

403 The partner account behind the key is not active (forbidden).

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 or unknown key (unauthorized).

application/json · error response

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

403 The partner account behind the key is not active (forbidden).

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 or unknown key (unauthorized).

application/json · error response

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

403 The partner account behind the key is not active (forbidden).

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 or unknown key (unauthorized).

application/json · error response

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

403 The partner account behind the key is not active (forbidden).

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"
}
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 or unknown key (unauthorized).

application/json · error response

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

403 The partner account behind the key is not active (forbidden).

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 or unknown key (unauthorized).

application/json · error response

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

403 The partner account behind the key is not active (forbidden).

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 or unknown key (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 or unknown key (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"
}