https://<your-portal-host>/api/partner/v1JSON requests and responses use camelCase. Money amounts are integer cents in USD. Each key belongs to one partner account.
Quickstart
- Create a test key in the partner portal under Developer API. Save the full key when shown.
- 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.
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_errorDeal 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_submittedapprovedboardinglivedeclinedclosedWebhooks
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.createdA deal credited to you was created.
deal.status_changedA deal's public status changed. Moves inside one public status never fire.
deal.approvedA deal moved to approved (also sends deal.status_changed).
deal.declinedA deal was declined (also sends deal.status_changed).
residuals.postedResiduals 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 FalseAPI reference
Endpoints below are relative to the base URL. Examples and response definitions come from the published OpenAPI specification.
/openapi.jsonSpecDownload this API specification
Responses
200 This document.
application/json
{
"type": "object"
}/meAccountIdentify 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"
}/dealsDealsSubmit 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
| Name | In | Required | Details |
|---|---|---|---|
| Idempotency-Key | header | No | Unique 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"
}/dealsDealsList your deals
Newest first. Page with cursor = the previous page's nextCursor.
Parameters
| Name | In | Required | Details |
|---|---|---|---|
| limit | query | No | {"type":"integer","minimum":1,"maximum":100,"default":25} |
| cursor | query | No | Return deals older than this id. |
| status | query | No | {"$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"
}/deals/{id}DealsGet one deal with its status history
Parameters
| Name | In | Required | Details |
|---|---|---|---|
| id | path | Yes | Deal 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"
}/residualsResidualsResiduals 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
| Name | In | Required | Details |
|---|---|---|---|
| from | query | No | Example: 2026-01 |
| to | query | No | Example: 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"
}/residuals/{month}ResidualsResiduals for one month
Parameters
| Name | In | Required | Details |
|---|---|---|---|
| month | path | Yes | Example: 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"
}/sandbox/deals/{id}/simulateSandboxMove 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
| Name | In | Required | Details |
|---|---|---|---|
| id | path | Yes | Deal 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"
}/sandbox/residuals/simulateSandboxFire 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"
}