Payarity

Payarity API

v1

Read your organization's commission and dispute data programmatically. Useful for building reports, piping data into a data warehouse, or wiring Payarity into your own tooling.

Authentication

All requests require an API key passed as a Bearer token. Generate keys in Settings → Developer API. Keys are org-scoped — they only return data for your organization.

curl https://dpkpxtbvxclycugpausg.supabase.co/functions/v1/api-v1/deals \
  -H "Authorization: Bearer pa_live_your_key_here"

Keep keys secret — treat them like passwords. Revoke and rotate them from Settings if they're ever exposed.

Scopes

Each key is created with one or more scopes that limit what it can access.

ScopeAccess
deals:readList and retrieve deals and commission data
deals:writeCreate/update deals — for a homegrown tool or custom data source
disputes:readList and retrieve open and resolved disputes

Rate limits

Each key is limited to 60 requests per minute, tracked in fixed 1-minute windows. Every response includes X-RateLimit-Limit and X-RateLimit-Remaining headers so you can back off before hitting the limit. If you exceed it, you'll get a 429 with a Retry-After header (seconds until the window resets).

HTTP/1.1 429 Too Many Requests
Retry-After: 23
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0

{"error": "Rate limit exceeded: max 60 requests per minute per API key."}

Errors

All errors return JSON with an error field and an appropriate HTTP status code.

StatusMeaning
400Malformed request body (bad JSON, empty/oversized deals array)
401Missing, invalid, or revoked API key
403Key doesn't have the required scope for this endpoint
404Resource not found (or not in your org)
405Method not allowed — GET for reads, POST only on /deals
422Every deal in the batch was rejected — see the errors array
429Rate limit exceeded — see Retry-After header
500Internal error — contact support if it persists

Deals

GEThttps://dpkpxtbvxclycugpausg.supabase.co/functions/v1/api-v1/dealsdeals:read

Returns a paginated list of deals for your organization, ordered by last_calculated descending.

Query parameters
NameTypeDescription
pageinteger0-indexed page number (default: 0)
limitintegerResults per page, max 100 (default: 50)
statusstringFilter by status: matched | exception | pending
Example
curl "https://dpkpxtbvxclycugpausg.supabase.co/functions/v1/api-v1/deals?status=exception&limit=10" \
  -H "Authorization: Bearer pa_live_your_key"
Response
{
  "data": [
    {
      "id": "uuid",
      "deal_name": "Acme Corp — Enterprise",
      "account": "Acme Corp",
      "owner": "Jane Smith",
      "period": "2025-Q1",
      "amount": 120000,
      "expected_commission": 6000,
      "actual_commission": 5660,
      "variance": -340,
      "status": "exception",
      "discrepancy_reason": "rate_mismatch",
      "close_date": "2025-03-15",
      "last_calculated": "2025-04-01T10:00:00Z"
    }
  ],
  "total": 47,
  "page": 0,
  "limit": 10
}
GEThttps://dpkpxtbvxclycugpausg.supabase.co/functions/v1/api-v1/deals/:iddeals:read

Returns a single deal by its UUID.

Example
curl "https://dpkpxtbvxclycugpausg.supabase.co/functions/v1/api-v1/deals/uuid-here" \
  -H "Authorization: Bearer pa_live_your_key"
Response
{
  "id": "uuid",
  "deal_name": "Acme Corp — Enterprise",
  "account": "Acme Corp",
  "owner": "Jane Smith",
  "period": "2025-Q1",
  "amount": 120000,
  "expected_commission": 6000,
  "actual_commission": 5660,
  "variance": -340,
  "status": "exception",
  "discrepancy_reason": "rate_mismatch",
  "close_date": "2025-03-15",
  "last_calculated": "2025-04-01T10:00:00Z"
}
POSThttps://dpkpxtbvxclycugpausg.supabase.co/functions/v1/api-v1/dealsdeals:write

Bulk create/update deals — for a homegrown commission tool, internal system, or anything else without a native connector. Send up to 500 deals per request as { deals: [...] } (a bare array also works). Include external_id — your own record ID for that deal — and re-posting it later updates the same row instead of creating a duplicate; omit it and the deal is always inserted fresh. A "Custom API" entry appears automatically on the Data Sources page after your first successful sync.

Body fields (per deal in the deals array)
NameTypeDescription
external_idstring, optionalYour own record ID — enables upsert-by-ID
deal_namestring, required
accountstring, required
ownerstring, required
amountnumber, requiredDeal amount
expected_commissionnumber, required
actual_commissionnumber, optionalDefaults to expected_commission if omitted
status"matched" | "exception", optionalAuto-computed from variance if omitted
discrepancy_reasonstring, optionalAuto-computed ("rate_mismatch"/"none") if omitted
territory, product, stage, tier, period, rate, close_date, crm_record_urloptional
Example
curl -X POST "https://dpkpxtbvxclycugpausg.supabase.co/functions/v1/api-v1/deals" \
  -H "Authorization: Bearer pa_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "deals": [
      {
        "external_id": "comp-engine-8842",
        "deal_name": "Acme Corp — Enterprise",
        "account": "Acme Corp",
        "owner": "Jane Smith",
        "amount": 120000,
        "expected_commission": 6000,
        "actual_commission": 5660
      }
    ]
  }'
Response
{
  "inserted": 0,
  "upserted": 1,
  "errors": []
}

Disputes

GEThttps://dpkpxtbvxclycugpausg.supabase.co/functions/v1/api-v1/disputesdisputes:read

Returns a paginated list of disputes for your organization, ordered by created_at descending.

Query parameters
NameTypeDescription
pageinteger0-indexed page number (default: 0)
limitintegerResults per page, max 100 (default: 50)
statusstringFilter by status: open | in_review | resolved
Example
curl "https://dpkpxtbvxclycugpausg.supabase.co/functions/v1/api-v1/disputes?status=open" \
  -H "Authorization: Bearer pa_live_your_key"
Response
{
  "data": [
    {
      "id": "uuid",
      "deal_id": "uuid",
      "status": "open",
      "created_at": "2025-04-02T08:30:00Z",
      "updated_at": "2025-04-02T08:30:00Z",
      "deals": {
        "deal_name": "Acme Corp — Enterprise",
        "account": "Acme Corp",
        "owner": "Jane Smith"
      }
    }
  ],
  "total": 3,
  "page": 0,
  "limit": 50
}
GEThttps://dpkpxtbvxclycugpausg.supabase.co/functions/v1/api-v1/disputes/:iddisputes:read

Returns a single dispute by its UUID.

Example
curl "https://dpkpxtbvxclycugpausg.supabase.co/functions/v1/api-v1/disputes/uuid-here" \
  -H "Authorization: Bearer pa_live_your_key"
Response
{
  "id": "uuid",
  "deal_id": "uuid",
  "status": "open",
  "created_at": "2025-04-02T08:30:00Z",
  "updated_at": "2025-04-02T08:30:00Z",
  "deals": {
    "deal_name": "Acme Corp — Enterprise",
    "account": "Acme Corp",
    "owner": "Jane Smith"
  }
}

Webhooks

Configure an endpoint in Settings → Webhooks and subscribe to the events you care about. Each event fires a single POST, best-effort — there's no retry queue yet, so a failed delivery won't be retried automatically. Recent deliveries (success/failure, response code) are visible per-endpoint in Settings.

Event types
EventFires when
dispute.raisedA new dispute is raised on a deal
dispute.status_changedA dispute moves to in review or resolved
deals.importedA CSV import finishes
statement.signoff_requestedReps are asked to sign off on statements for a period
statement.signedA rep signs off on their commission statement
statement.manager_approvedA manager approves a report's statement
statement.paidA commission statement is marked paid
Payload

Every delivery is a POST with a JSON body shaped like this, plus X-Payarity-Event and X-Payarity-Signature headers.

{
  "event": "dispute.status_changed",
  "timestamp": "2025-04-02T08:30:00Z",
  "data": {
    "dispute_id": "uuid",
    "deal_id": "uuid",
    "deal_name": "Acme Corp — Enterprise",
    "previous_status": "open",
    "status": "in_review"
  }
}
Verifying the signature

X-Payarity-Signature is sha256=<hex>, an HMAC-SHA256 of the raw request body using your endpoint's secret (shown in Settings). Recompute it and compare — don't parse the body first, sign the exact bytes you received.

// Node.js
const crypto = require("crypto");

function isValid(rawBody, signatureHeader, secret) {
  const expected = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));
}
Questions or feedback? Open a dispute in the app or contact support.