{"openapi":"3.1.0","info":{"title":"OpenPartner API","version":"1.0.0","description":"The OpenPartner tenant API: partners, programs, links, attribution, commissions, payouts, reporting, webhooks and export. Use it to run a referral or affiliate program from your own backend, for example an in-app \"Refer & earn\" page with no separate OpenPartner login. Walkthrough: [Build an in-app referral page](https://github.com/getcoherence/openpartner/blob/main/docs/headless-referral.md).\n\nThis document is generated from the API's own request validators. It is served at `/openapi.json`, and the rendered reference at `/docs`, on every deployment. White-label custom domains don't serve them; use the platform host, e.g. `https://app.openpartner.dev/api/docs`.\n\n## Base URL and tenancy\n\nEvery path below is relative to your tenant's API base:\n\n| Deployment | API base | Share links |\n| --- | --- | --- |\n| OpenPartner Cloud | `https://app.openpartner.dev/api/t/<tenant-slug>` | `https://app.openpartner.dev/r/<tenant-slug>/<linkKey>` |\n| Cloud with a white-label domain | `https://<your-domain>/api` | `https://<your-domain>/r/<linkKey>` |\n| Self-hosted, single tenant | your API origin, e.g. `https://api.example.com` (or `<portal>/api` behind the bundled proxy) | `<router origin>/r/<linkKey>` |\n| Self-hosted, multi-tenant | `<api origin>/t/<tenant-slug>` (or `<portal>/api/t/<tenant-slug>`) | `<router origin>/r/<tenant-slug>/<linkKey>` |\n\nThe slug comes **after** `/api`. `https://app.openpartner.dev/t/<slug>/api/...` reaches the portal's static site, not the API, and returns HTML. On a multi-tenant deployment a request with no `/t/<slug>` prefix fails with `400 tenant_slug_missing`, and an unknown slug gets a plain `404`. Your slug is the `/t/<slug>/` segment in your admin portal's address.\n\n**Publisher sub-IDs.** A partner may add `?sub_id=<their own id>` to a share link (trimmed, at most 255 characters). The router stores it on the click and doesn't pass it to your destination. It comes back as `subId` on `commission.*` webhooks and in the partner's `{sub_id}` postback macro, so a publisher's tracking tool can match each conversion to its own click.\n\nKeys minted through the API are bound to their tenant. A key sent to another tenant's path is `401 unauthorized`.\n\n## Authentication\n\nSend a key as a bearer token: `Authorization: Bearer op_…`. Keys are server-side credentials: never ship one to a browser or mobile app. There are three kinds:\n\n- **Admin key.** Unscoped, full access to the tenant. Minted with `POST /api-keys`, or the deployment's `ADMIN_API_KEY` (self-host). The admin portal's session cookie is accepted wherever an admin key is.\n- **Scoped key.** Minted with `POST /api-keys/scoped` and a list of scopes. It can call only the operations its scopes unlock, and it acts on any partner in the tenant. Calling something it lacks the scope for returns `403 forbidden_scope` with the `required` scope; calling an operation with no scope returns `403 forbidden`. Use one for every integration so a leaked key can't do everything.\n- **Partner key.** Minted with `POST /partners/{id}/api-keys` by an admin or the partner. It acts as that one partner and can only reach that partner's own resources (`/partners/{its id}/…`). Revoking the partner revokes its keys.\n\nEach operation says which of these it accepts. Scopes:\n\n| Scope | Unlocks |\n| --- | --- |\n| `bonuses:write` | `POST /bonuses`<br>`GET /bonuses`<br>`GET /bonuses/{id}` |\n| `clicks:write` | `POST /clicks` |\n| `commissions:read` | `GET /partners/{id}/commissions`<br>`GET /commissions`<br>`GET /bonuses`<br>`GET /bonuses/{id}` |\n| `events:write` | `POST /attribution/events`<br>`POST /attribution/conversions`<br>`POST /attribution/refunds`<br>`POST /coupons/redeem` |\n| `links:read` | `GET /partners/{id}/links` |\n| `links:write` | `POST /partners/{id}/links` |\n| `partners:create` | `POST /partners` |\n| `partners:read` | `GET /partners`<br>`GET /partners/{id}`<br>`GET /partners/{id}/recruits`<br>`GET /partners/{id}/dashboard`<br>`GET /partners/{id}/timeseries`<br>`GET /partners/{id}/events`<br>`GET /partners/{id}/customers`<br>`GET /partners/{id}/breakdowns`<br>`GET /partners/{id}/referrals` |\n| `partners:signin` | `POST /partners/{id}/signin-link`<br>`POST /partners/signin-link` |\n| `partners:write` | `POST /partners`<br>`PATCH /partners/{id}`<br>`POST /import/partners-csv`<br>`POST /partners/{id}/invite`<br>`POST /partners/{id}/revoke`<br>`POST /partners/{id}/coupons`<br>`DELETE /partners/{id}/coupons/{couponId}` |\n| `payout_profiles:read` | `GET /partners/{id}/payout-profile` |\n| `payout_profiles:write` | `GET /partners/{id}/payout-profile`<br>`PUT /partners/{id}/payout-profile`<br>`DELETE /partners/{id}/payout-profile` |\n| `payouts:read` | `GET /payouts`<br>`GET /partners/{id}/payouts`<br>`GET /payouts/{id}/statement` |\n| `webhooks:write` | `POST /webhooks`<br>`DELETE /webhooks/{id}` |\n\n`partners:read` also unlocks `GET /brand-resources` (brand logo, colours and shareable assets), which this reference doesn't cover yet. The OpenPartner Network federation key holds `partners:write`, `partners:read`, `links:write`, `commissions:read`, `clicks:write`, `bonuses:write`.\n\n**Scoped reads that don't exist yet:** `GET /partners/{id}/funnel`, `/coupons` and `/commission-snapshot` accept no scope. Read them with an admin key from your server, or use `/timeseries` (which has `partners:read`) for funnel-like counts.\n\n## Requests and responses\n\n- JSON bodies (`content-type: application/json`), up to 1 MB. `POST /import` takes up to 256 MB on single-tenant installs.\n- Errors are `{ \"error\": \"<code>\", \"detail\": … }`. Branch on `error`; `detail` is for humans.\n- **Timestamps are ISO-8601 in UTC and must end in `Z`**: `2026-10-16T10:00:00Z`. An offset such as `+00:00` (Python's `isoformat()` default) is rejected with `400 invalid_body` / `invalid_query`.\n- Money in major units. Commission and payout amounts, and `eventValue` in webhook payloads, are decimal **strings** such as `\"12.50\"`. Report endpoints (dashboard, timeseries, a partner's events list) return JSON numbers.\n- Fields may be added to any response at any time. Ignore fields you don't know.\n- Send `X-Request-Id` to correlate a call; every response echoes one back (generated if you didn't send it). Quote it in support requests.\n\n## Idempotency\n\n`POST /attribution/events`, `POST /attribution/conversions`, `POST /attribution/refunds` and `POST /coupons/redeem` take an `externalEventId`: your stable id for that one event (required on conversions and refunds). A retry with the same key replays the original result (`replayed: true`) and records nothing, even when the duplicates arrive at the same moment. A key reused for a different event is `409 external_event_id_conflict`. `POST /clicks` is idempotent on the click `id`, and `POST /bonuses` on its optional `externalRef` (reused for a different partner, program, amount or currency: `409 external_ref_conflict`). Other writes are not idempotent: `POST /partners` answers a duplicate email with `409 email_taken`, so look the partner up with `GET /partners?email=`.\n\n## Pagination\n\n`GET /partners`, the commission lists and `GET /payouts` are keyset-paginated: pass the response's `nextCursor` as `cursor` until it is `null`. Cursors are opaque. `GET /partners/{id}/events` and `/customers` use `limit` + `offset`. `GET /partners/{id}/payouts` returns the 200 most recent, and `GET /webhooks/{id}/deliveries` the 100 most recent.\n\n## Rate limits\n\nThe API shares one limit of **600 requests per 5 minutes per client IP** across its routes (a few infrastructure paths are exempt). A backend calls from one IP, so that is your whole integration's budget, event ingest included: at four calls per page view, about 150 uncached page views per 5 minutes. Cache partner stats for a few minutes, don't let users force a refresh, and report events from a queue that backs off on `429`. Over the limit you get `429 rate_limited` with the IETF `RateLimit` and `RateLimit-Policy` headers.\n\n`POST /attribution/identify` has its own limit of 120 per minute per IP, answered with `429 rate_limited` and `Retry-After`. When the browser SDK calls it, that is each visitor's IP; when your backend calls it, every stitch counts against your backend's IP, so prefer the browser for identify at volume.\n\n## Webhooks\n\nSubscribe with `POST /webhooks`; the event catalogue is under **Webhook events**. Each delivery is a JSON `POST` of an envelope `{ id, event, created, data }` with:\n\n| Header | Value |\n| --- | --- |\n| `X-OpenPartner-Signature` | hex HMAC-SHA256 of `<timestamp>.<raw body>`, keyed with the endpoint's secret (the whole `whsec_…` string) |\n| `X-OpenPartner-Timestamp` | Unix seconds when the attempt was signed |\n| `X-OpenPartner-Event` | the event type |\n| `X-OpenPartner-Delivery` | the envelope `id` |\n\nTo verify a delivery, recompute the HMAC over the **raw** request body, before any JSON parsing, and compare it in constant time. Reject timestamps more than 5 minutes from now. Then dedupe on `id`:\n\n```js\nimport { createHmac, timingSafeEqual } from 'node:crypto';\n\nfunction verify(rawBody, headers, secret) {\n  const ts = headers['x-openpartner-timestamp'];\n  const sig = headers['x-openpartner-signature'] ?? '';\n  if (!ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;\n  const expected = Buffer.from(createHmac('sha256', secret).update(`${ts}.${rawBody}`).digest('hex'));\n  const given = Buffer.from(sig);\n  return given.length === expected.length && timingSafeEqual(given, expected);\n}\n```\n\nDelivery is at most once per trigger: a non-2xx answer, a redirect, or no answer within 10 seconds marks the delivery failed, and **failed deliveries are not retried automatically**. Find them with `GET /webhooks/{id}/deliveries` and resend with `POST /webhooks/{id}/deliveries/{deliveryId}/retry`. A resend carries the same `id` and a fresh signature. Treat webhooks as notifications and the list endpoints as the record: reconcile from `GET /commissions` if you can't afford to miss one.\n\nEndpoints must be publicly reachable. Private, loopback and link-local destinations are refused at delivery time, and OpenPartner Cloud delivers only to ports 80 and 443. A single-tenant self-host can allow specific private ranges with `OPENPARTNER_OUTBOUND_ALLOW_PRIVATE_CIDRS`.","license":{"name":"MIT","identifier":"MIT"}},"servers":[{"url":"https://app.openpartner.dev/api/t/{tenantSlug}","description":"OpenPartner Cloud","variables":{"tenantSlug":{"default":"your-tenant-slug","description":"The `/t/<slug>/` segment of your admin portal address."}}},{"url":"https://{customDomain}/api","description":"OpenPartner Cloud on a white-label custom domain","variables":{"customDomain":{"default":"partners.example.com"}}},{"url":"{apiOrigin}","description":"Self-hosted, single tenant","variables":{"apiOrigin":{"default":"http://localhost:4601"}}}],"tags":[{"name":"Partners","description":"The people (or customers) who refer. Create them headless with `sendInvite: false`."},{"name":"Programs","description":"Commission terms, attribution window and model, destination URL."},{"name":"Program assignments","description":"Which programs a partner may create links for."},{"name":"Links","description":"Share links. The click router records a click and appends `?cref=<click id>` to the destination."},{"name":"Attribution","description":"Stitch clicks to users, then report conversions server-side."},{"name":"Coupons","description":"Code-based attribution, for checkouts where a link can't be followed."},{"name":"Commissions","description":"The commission ledger: accrued → approved → paid, or reversed."},{"name":"Bonuses","description":"Rewards not tied to a sale. Each is paid through one commission."},{"name":"Payouts","description":"Paying approved commissions, via Stripe Connect or a manual rail."},{"name":"Partner reporting","description":"Per-partner stats for your own UI."},{"name":"Webhooks","description":"Subscribe endpoints to the events under **Webhook events**."},{"name":"API keys","description":"Admin, scoped and partner keys."},{"name":"Export","description":"Your data, in formats a self-hosted instance re-imports."},{"name":"Network settings","description":"The tenant's OpenPartner Network membership, including whether partners are offered the Network."},{"name":"Webhook events","description":"What OpenPartner POSTs to your webhook endpoints."}],"components":{"securitySchemes":{"adminKey":{"type":"http","scheme":"bearer","description":"An unscoped admin key (`POST /api-keys`), or the deployment's `ADMIN_API_KEY`. Full access to the tenant it was minted in. Server-side only."},"scopedKey":{"type":"http","scheme":"bearer","description":"A scoped key (`POST /api-keys/scoped`). Each operation lists the one scope that unlocks it; a key without it gets `403 forbidden_scope`. Operations that list no scope reject scoped keys."},"partnerKey":{"type":"http","scheme":"bearer","description":"A partner-scoped key (`POST /partners/{id}/api-keys`). Acts as that one partner: it can read and write only that partner's own resources."},"postbackToken":{"type":"apiKey","in":"query","name":"token","description":"The conversion postback token (`opcp_…`), minted and rotated under Admin → Integrations. It travels in the URL, so proxies, browser history and the sender's own logs keep it. It is built to be cheap to leak: it records conversions and nothing else, is never accepted as a Bearer key, can be pinned to the sender's IPs, and rotating it kills the old URL at once. Only `GET /postback` takes it."}},"schemas":{"PartnerCreated":{"allOf":[{"$ref":"#/components/schemas/Partner"},{"type":"object","properties":{"invited":{"type":"boolean"}},"required":["invited"]}]},"Partner":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"email":{"type":"string","format":"email"},"name":{"type":"string"},"stripeConnectAccountId":{"type":["string","null"]},"metadata":{"type":"object","additionalProperties":{},"description":"Free-form JSON you supplied at creation. OpenPartner may add a `network` key when the partner is synced to the Network."},"createdAt":{"type":"string","format":"date-time","pattern":"Z$"},"updatedAt":{"type":"string","format":"date-time","pattern":"Z$"},"activatedAt":{"type":["string","null"],"format":"date-time","description":"Null until the partner accepts their invite. `sendInvite: false` sets it at creation.","pattern":"Z$"},"revokedAt":{"type":["string","null"],"format":"date-time","pattern":"Z$"},"revokeReason":{"type":["string","null"]},"portalAccess":{"type":"boolean","description":"May the partner sign in to the partner portal? `false`: `POST /auth/signin` sends them nothing (answering as it does for an unknown email), password and Google sign-in turn them away the same way, their sessions end, and no invite or portal email goes to them. For headless programs. Set it on `POST /partners` or `PATCH /partners/{id}`."},"recruitedByPartnerId":{"type":["string","null"],"description":"The partner who recruited this one: the one whose recruit link they joined through, or the one set on `POST /partners` or `PATCH /partners/{id}`. In programs with `recruiterCommissionPercent` that partner earns that share of this one's commissions. Null when nobody did."},"recruitedAt":{"type":["string","null"],"format":"date-time","description":"When the recruitment started. Only commissions on events from then on pay the recruiter, and `recruiterCommissionMonths` counts from here.","pattern":"Z$"},"recruitCode":{"type":["string","null"],"description":"This partner's own code for recruiting others: the `ref` on their join-page link. Minted the first time `GET /partners/{id}/recruits` is read while a program pays a recruiter share."}},"required":["id","tenantId","email","name","stripeConnectAccountId","metadata","createdAt","updatedAt","activatedAt","revokedAt","revokeReason","portalAccess","recruitedByPartnerId","recruitedAt","recruitCode"]},"Error":{"type":"object","properties":{"error":{"type":"string","description":"Stable machine-readable code, e.g. `invalid_body`, `email_taken`."},"detail":{"description":"Human-readable cause, or zod `flatten()` output for `invalid_body` / `invalid_query`."}},"required":["error"],"additionalProperties":{},"description":"Every non-2xx response. `error` is stable and safe to branch on; `detail` is for humans and may change. Some errors carry extra fields (e.g. `required` on `forbidden_scope`, `matureAt` on `holdback_active`)."},"CreatePartnerRequest":{"type":"object","properties":{"email":{"type":"string","format":"email"},"name":{"type":"string","minLength":1},"metadata":{"type":"object","additionalProperties":{}},"sendInvite":{"type":"boolean"},"portalAccess":{"type":"boolean"},"programIds":{"type":"array","items":{"type":"string"}},"campaignIds":{"type":"array","items":{"type":"string"}},"campaignGrantSource":{"type":"string","enum":["admin","offering"]},"commissionSnapshot":{"type":"object","properties":{"commissionType":{"type":"string","enum":["percent","fixed"]},"commissionValue":{"type":"number","minimum":0},"recurring":{"type":"boolean"},"holdbackDays":{"type":"integer","minimum":0},"commissionRules":{"type":"array","items":{"type":"object","properties":{"trigger":{"type":"string","enum":["every","first","subsequent"]},"eventType":{"type":"string","minLength":1,"maxLength":80},"type":{"type":"string","enum":["percent","fixed"]},"value":{"type":"number","minimum":0},"currency":{"type":"string","minLength":3,"maxLength":3},"recurring":{"type":"boolean"},"recurringMonths":{"type":"integer","exclusiveMinimum":0,"maximum":600},"bands":{},"tiers":{}},"required":["trigger","type","value"]},"minItems":1,"maxItems":10}}},"programCommissions":{"type":"array","items":{"type":"object","properties":{"commissionType":{"type":"string","enum":["percent","fixed"]},"commissionValue":{"type":"number","minimum":0},"recurring":{"type":"boolean"},"holdbackDays":{"type":["integer","null"],"minimum":0},"commissionRules":{"type":"array","items":{"type":"object","properties":{"trigger":{"type":"string","enum":["every","first","subsequent"]},"eventType":{"type":"string","minLength":1,"maxLength":80},"type":{"type":"string","enum":["percent","fixed"]},"value":{"type":"number","minimum":0},"currency":{"type":"string","pattern":"^[A-Za-z]{3}$"},"recurring":{"type":"boolean"},"recurringMonths":{"type":"integer","exclusiveMinimum":0,"maximum":600},"bands":{"type":"object","properties":{"mode":{"type":"string","enum":["whole","marginal"],"default":"whole"},"levels":{"type":"array","items":{"type":"object","properties":{"above":{"type":"number","exclusiveMinimum":0},"value":{"type":"number","minimum":0}},"required":["above","value"]},"minItems":1,"maxItems":10}},"required":["levels"]},"tiers":{"type":"object","properties":{"metric":{"type":"string","enum":["revenue","conversions"]},"window":{"type":"string","enum":["lifetime","calendar_month","rolling_days"]},"windowDays":{"type":"integer","exclusiveMinimum":0,"maximum":3650},"eventType":{"type":"string","minLength":1,"maxLength":80},"levels":{"type":"array","items":{"type":"object","properties":{"atLeast":{"type":"number","exclusiveMinimum":0},"value":{"type":"number","minimum":0}},"required":["atLeast","value"]},"minItems":1,"maxItems":10}},"required":["metric","window","levels"]}},"required":["trigger","type","value"]},"minItems":1,"maxItems":10},"programId":{"type":"string","minLength":1}},"required":["commissionType","commissionValue","programId"]},"maxItems":100},"country":{"type":"string"},"outsideOccupiedRegions":{"type":"boolean"},"recruitedByPartnerId":{"type":"string","minLength":1}},"required":["email","name"]},"PartnerPage":{"type":"object","properties":{"partners":{"type":"array","items":{"$ref":"#/components/schemas/Partner"}},"nextCursor":{"type":["string","null"],"description":"Pass as `cursor` for the next page. Null on the last page."}},"required":["partners","nextCursor"]},"UpdatePartnerRequest":{"type":"object","properties":{"portalAccess":{"type":"boolean"},"recruitedByPartnerId":{"type":["string","null"],"minLength":1}},"additionalProperties":false},"PartnerRecruits":{"type":"object","properties":{"partnerId":{"type":"string"},"recruitCode":{"type":["string","null"],"description":"The `ref` on their recruit link. Null until a program pays a recruiter share."},"recruitUrl":{"type":["string","null"],"description":"The join page with the partner's `ref`. Null while no program they can recruit for pays a recruiter share, or the partner is revoked."},"signupOpen":{"type":"boolean","description":"False when the join page is closed: the link then opens a page that says so."},"programs":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"recruiterCommissionPercent":{"type":"number"},"recruiterCommissionMonths":{"type":["integer","null"]}},"required":["id","name","recruiterCommissionPercent","recruiterCommissionMonths"]},"description":"Programs that pay a recruiter share. A partner calling sees only running programs they are granted; an admin sees all of them."},"recruitedBy":{"type":["object","null"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"recruitedAt":{"type":"string","format":"date-time","pattern":"Z$"}},"required":["id","name","recruitedAt"],"description":"Who recruited this partner, if anyone."},"recruits":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"recruitedAt":{"type":"string","format":"date-time","pattern":"Z$"},"status":{"type":"string","enum":["active","pending","revoked"],"description":"`pending`: not active yet (invite not accepted, or held for review)."},"earned":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/Decimal"},"description":"What this recruit has earned the recruiter, per currency: live recruiter commissions net of clawbacks."}},"required":["id","name","recruitedAt","status","earned"]}},"total":{"type":"integer","description":"How many partners this one recruited. `recruits` lists the newest `limit`."},"limit":{"type":"integer"}},"required":["partnerId","recruitCode","recruitUrl","signupOpen","programs","recruitedBy","recruits","total","limit"]},"Decimal":{"type":"string","pattern":"^-?\\d+(\\.\\d+)?$","description":"A decimal amount as a string, e.g. `\"12.50\"`. Major currency units.","example":"12.50"},"PartnerSigninLink":{"type":"object","properties":{"url":{"type":"string","format":"uri","description":"Redirect the partner's browser here. It signs them in to the partner portal, once, and lands on `returnTo` (or their dashboard). On your white-label domain when one is verified."},"expiresAt":{"type":"string","format":"date-time","description":"Five minutes after minting. Unused by then, the link fails.","pattern":"Z$"},"partnerId":{"type":"string"}},"required":["url","expiresAt","partnerId"]},"PartnerSigninLinkRequest":{"type":"object","properties":{"returnTo":{"type":"string","maxLength":512,"pattern":"^\\/(?!\\/)[\\x21-\\x5b\\x5d-\\x7e]*$"}},"additionalProperties":false},"PartnerSigninLinkLookupRequest":{"anyOf":[{"type":"object","properties":{"email":{"type":"string","format":"email"},"returnTo":{"type":"string","maxLength":512,"pattern":"^\\/(?!\\/)[\\x21-\\x5b\\x5d-\\x7e]*$"}},"required":["email"],"additionalProperties":false},{"type":"object","properties":{"metadata":{"type":"object","additionalProperties":{"anyOf":[{"type":"string","minLength":1,"maxLength":200},{"type":"number"}]}},"returnTo":{"type":"string","maxLength":512,"pattern":"^\\/(?!\\/)[\\x21-\\x5b\\x5d-\\x7e]*$"}},"required":["metadata"],"additionalProperties":false}]},"RevokePartnerRequest":{"type":"object","properties":{"reason":{"type":"string","maxLength":500},"notify":{"type":"boolean"}}},"Ok":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]}},"required":["ok"]},"CommissionSnapshot":{"type":"object","properties":{"id":{"type":"string"},"partnerId":{"type":"string"},"programId":{"type":["string","null"],"description":"Null for partner-wide terms; set for terms on that one program."},"commissionType":{"type":"string","enum":["percent","fixed"]},"commissionValue":{"$ref":"#/components/schemas/Decimal"},"recurring":{"type":"boolean"},"commissionRules":{"type":["array","null"],"items":{"type":"object","properties":{"trigger":{"type":"string","enum":["every","first","subsequent"]},"eventType":{"type":"string"},"type":{"type":"string","enum":["percent","fixed"]},"value":{"type":"number"},"currency":{"type":"string"},"recurring":{"type":"boolean","description":"Keeps paying on renewals, including renewals past the attribution window that the program's `renewalsStayWithPartner` carries. A rule without it pays only while the click that earned the customer is inside the window."},"recurringMonths":{"type":"integer","description":"Stop paying this many months after the customer's first attributed event of the rule's type for this partner. Omitted: no cap."},"bands":{"$ref":"#/components/schemas/CommissionBands"},"tiers":{"$ref":"#/components/schemas/CommissionTiers"}},"required":["trigger","type","value"]}},"holdbackDays":{"type":["integer","null"]},"source":{"type":"string","enum":["approval","backfill","amendment"]},"snapshottedAt":{"type":"string","format":"date-time","pattern":"Z$"}},"required":["id","partnerId","programId","commissionType","commissionValue","recurring","commissionRules","holdbackDays","source","snapshottedAt"],"description":"A per-partner commission override. When present it replaces the program's rule for this partner's future commissions: on every program (partner-wide, `programId` null) or on one program. On that program, program-scoped terms beat partner-wide ones."},"CommissionBands":{"type":"object","properties":{"mode":{"type":"string","enum":["whole","marginal"],"default":"whole"},"levels":{"type":"array","items":{"type":"object","properties":{"above":{"type":"number","exclusiveMinimum":0},"value":{"type":"number","minimum":0}},"required":["above","value"]},"minItems":1,"maxItems":10}},"required":["levels"],"description":"Value bands: the rate steps up with the event's value. The sub-rule's own `value` is the base band; `levels` lists the steps above it. `whole` pays the reached band's rate on the whole value, `marginal` pays each slice at its own band's rate."},"CommissionTiers":{"type":"object","properties":{"metric":{"type":"string","enum":["revenue","conversions"]},"window":{"type":"string","enum":["lifetime","calendar_month","rolling_days"]},"windowDays":{"type":"integer","exclusiveMinimum":0,"maximum":3650},"eventType":{"type":"string","minLength":1,"maxLength":80},"levels":{"type":"array","items":{"type":"object","properties":{"atLeast":{"type":"number","exclusiveMinimum":0},"value":{"type":"number","minimum":0}},"required":["atLeast","value"]},"minItems":1,"maxItems":10}},"required":["metric","window","levels"],"description":"Performance tiers: the rate steps up with the partner's own revenue or conversions over a window. Not combined with `bands`."},"CommissionOverrideRequest":{"type":"object","properties":{"commissionType":{"type":"string","enum":["percent","fixed"]},"commissionValue":{"type":"number","minimum":0},"recurring":{"type":"boolean"},"holdbackDays":{"type":["integer","null"],"minimum":0},"commissionRules":{"type":"array","items":{"type":"object","properties":{"trigger":{"type":"string","enum":["every","first","subsequent"]},"eventType":{"type":"string","minLength":1,"maxLength":80},"type":{"type":"string","enum":["percent","fixed"]},"value":{"type":"number","minimum":0},"currency":{"type":"string","pattern":"^[A-Za-z]{3}$"},"recurring":{"type":"boolean"},"recurringMonths":{"type":"integer","exclusiveMinimum":0,"maximum":600},"bands":{"type":"object","properties":{"mode":{"type":"string","enum":["whole","marginal"],"default":"whole"},"levels":{"type":"array","items":{"type":"object","properties":{"above":{"type":"number","exclusiveMinimum":0},"value":{"type":"number","minimum":0}},"required":["above","value"]},"minItems":1,"maxItems":10}},"required":["levels"]},"tiers":{"type":"object","properties":{"metric":{"type":"string","enum":["revenue","conversions"]},"window":{"type":"string","enum":["lifetime","calendar_month","rolling_days"]},"windowDays":{"type":"integer","exclusiveMinimum":0,"maximum":3650},"eventType":{"type":"string","minLength":1,"maxLength":80},"levels":{"type":"array","items":{"type":"object","properties":{"atLeast":{"type":"number","exclusiveMinimum":0},"value":{"type":"number","minimum":0}},"required":["atLeast","value"]},"minItems":1,"maxItems":10}},"required":["metric","window","levels"]}},"required":["trigger","type","value"]},"minItems":1,"maxItems":10}},"required":["commissionType","commissionValue"]},"ProgramList":{"type":"object","properties":{"programs":{"type":"array","items":{"$ref":"#/components/schemas/Program"}},"customerRewardDelivery":{"type":"string","enum":["stripe","unconfigured","brand"],"description":"How customer-side rewards reach the customer on this deployment: minted in Stripe, not configured, or applied by the brand itself."}},"required":["programs","customerRewardDelivery"]},"Program":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"name":{"type":"string"},"commissionRule":{"$ref":"#/components/schemas/CommissionRule"},"attributionWindowDays":{"type":"integer","description":"How old a click may be and still win a customer's first conversion of an event type. With `renewalsStayWithPartner`, it doesn't limit renewals."},"attributionModel":{"type":"string","enum":["last_click","first_click","linear","position"]},"renewalsStayWithPartner":{"type":"boolean","description":"True (default): a repeat event, meaning a type the customer already has an attributed conversion of (the 2nd, 13th, 40th `invoice_paid`), keeps that first conversion's partners, weights and model whatever the click's age, and a later click from another partner doesn't take it. Past the window only `recurring` sub-rules pay on it, up to `recurringMonths`. False: every event is attributed on its own against the clicks inside the window."},"couponsYieldToLinks":{"type":"boolean","description":"False (default): a coupon code redeemed at checkout counts as a click at that moment, so under `last_click` it beats an earlier link click from another partner. True: this program's coupon codes drop out of a conversion whenever the customer has a partner link click inside the window, so a code credits only when no link did. Decided per code, by the program it belongs to. It decides who wins a customer's first conversion of each type; with `renewalsStayWithPartner`, renewals follow that winner, so changing it doesn't move an existing customer's renewals."},"destinationUrl":{"type":"string","format":"uri"},"deepLinkAllowedDomains":{"type":["string","null"]},"holdbackDays":{"type":["integer","null"],"description":"Above 0: commissions auto-approve this many days after accruing. 0 or null: they wait for a manual approve."},"startsAt":{"type":["string","null"],"format":"date-time","pattern":"Z$"},"endsAt":{"type":["string","null"],"format":"date-time","pattern":"Z$"},"endNotificationSentAt":{"type":["string","null"],"format":"date-time","pattern":"Z$"},"customerReward":{"anyOf":[{"$ref":"#/components/schemas/CustomerReward"},{"type":"null"}]},"categories":{"type":"array","items":{"type":"string"}},"partnersMayCustomizeCode":{"type":"boolean"},"revenueEventTypes":{"type":"array","items":{"type":"string"},"description":"Event types that are sales in this program beyond invoice_paid and subscription_created, which always are. Their value counts as partner-attributed revenue for the hosted platform fee."},"recruiterCommissionPercent":{"anyOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}],"description":"Two-tier commissions: a partner's recruiter earns this percent of each commission the partner earns in this program (`\"10.00\"` = 10%). Null: off, the default. Only one level: a recruiter earns nothing on partners their recruits bring in."},"recruiterCommissionMonths":{"type":["integer","null"],"description":"How many months after the recruitment the recruiter keeps earning it. Null: for as long as the recruit earns."},"shareOnNetwork":{"type":"boolean"},"marketplaceDescription":{"type":["string","null"]},"marketplaceCoverUrl":{"type":["string","null"]},"marketplaceVideoUrl":{"type":["string","null"]},"networkOfferingId":{"type":["string","null"]},"blockedAt":{"type":["string","null"],"format":"date-time","pattern":"Z$"},"blockedReason":{"type":["string","null"]},"blockedByEmail":{"type":["string","null"]},"createdAt":{"type":"string","format":"date-time","pattern":"Z$"}},"required":["id","tenantId","name","commissionRule","attributionWindowDays","attributionModel","renewalsStayWithPartner","couponsYieldToLinks","destinationUrl","deepLinkAllowedDomains","holdbackDays","startsAt","endsAt","endNotificationSentAt","customerReward","categories","partnersMayCustomizeCode","revenueEventTypes","recruiterCommissionPercent","recruiterCommissionMonths","shareOnNetwork","marketplaceDescription","marketplaceCoverUrl","marketplaceVideoUrl","networkOfferingId","blockedAt","blockedReason","blockedByEmail","createdAt"]},"CommissionRule":{"anyOf":[{"type":"array","items":{"type":"object","properties":{"trigger":{"type":"string","enum":["every","first","subsequent"]},"eventType":{"type":"string"},"type":{"type":"string","enum":["percent","fixed"]},"value":{"type":"number"},"currency":{"type":"string"},"recurring":{"type":"boolean","description":"Keeps paying on renewals, including renewals past the attribution window that the program's `renewalsStayWithPartner` carries. A rule without it pays only while the click that earned the customer is inside the window."},"recurringMonths":{"type":"integer","description":"Stop paying this many months after the customer's first attributed event of the rule's type for this partner. Omitted: no cap."},"bands":{"$ref":"#/components/schemas/CommissionBands"},"tiers":{"$ref":"#/components/schemas/CommissionTiers"}},"required":["trigger","type","value"]}},{"type":"object","properties":{"type":{"type":"string","enum":["percent","fixed"]},"value":{"type":"number"},"currency":{"type":"string"},"recurring":{"type":"boolean"}},"required":["type","value"]}],"description":"An array of triggered sub-rules. Programs written through this API always store the array; rows created before compound rules may still hold the legacy single-rule object."},"CustomerReward":{"type":"object","properties":{"type":{"type":"string","enum":["percent_off","amount_off","free_months"]},"value":{"type":"number"},"currency":{"type":"string"},"duration":{"type":"string","enum":["once","forever","repeating"]},"durationInMonths":{"type":"integer"}},"required":["type","value","duration"]},"ProgramStatsList":{"type":"object","properties":{"since":{"type":"string","format":"date-time","pattern":"Z$"},"until":{"type":"string","format":"date-time","pattern":"Z$"},"currency":{"type":"string","description":"The brand's default currency, for showing a zero. Amounts are in their own currencies.","example":"USD"},"programs":{"type":"array","items":{"type":"object","properties":{"programId":{"type":"string"},"name":{"type":"string"},"partners":{"type":"integer","description":"Partners enrolled in the program now, plus any with clicks, conversions or commission in it during the window."},"clicks":{"type":"integer"},"attributedEvents":{"type":"integer","description":"Conversions attributed in the window, counted per attribution: an event credited through two clicks (a multi-touch model) counts twice."},"attributedRevenueByCurrency":{"type":"object","additionalProperties":{"type":"number"},"description":"Amount per ISO 4217 currency code. Currencies with nothing are absent; nothing is converted."},"commissionByCurrency":{"type":"object","additionalProperties":{"type":"object","properties":{"accrued":{"type":"number"},"approved":{"type":"number"},"paid":{"type":"number"},"reversed":{"type":"number"}}},"description":"Commission totals per currency, then per status. Currencies and statuses with nothing are absent."}},"required":["programId","name","partners","clicks","attributedEvents","attributedRevenueByCurrency","commissionByCurrency"]},"description":"Every program, newest first, including programs with no activity."}},"required":["since","until","currency","programs"]},"PartnerVisibleProgram":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"destinationUrl":{"type":"string","format":"uri"},"deepLinkAllowedDomains":{"type":["string","null"]},"startsAt":{"type":["string","null"],"format":"date-time","pattern":"Z$"},"endsAt":{"type":["string","null"],"format":"date-time","pattern":"Z$"},"partnersMayCustomizeCode":{"type":"boolean"},"customerReward":{"anyOf":[{"$ref":"#/components/schemas/CustomerReward"},{"type":"null"}]},"source":{"type":"string","enum":["admin","offering"],"description":"Partner callers only: how the grant was made."}},"required":["id","name","destinationUrl","deepLinkAllowedDomains","startsAt","endsAt","partnersMayCustomizeCode","customerReward"]},"ProgramSaved":{"allOf":[{"$ref":"#/components/schemas/Program"},{"type":"object","properties":{"warnings":{"$ref":"#/components/schemas/SaveWarnings"}}}]},"SaveWarnings":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string","enum":["destinationUrl"]},"message":{"type":"string"}},"required":["field","message"]},"description":"Present only when there is something to fix. Today: a Stripe Payment Link (`buy.stripe.com`, `checkout.stripe.com`) without `client_reference_id={click_id}`. Stripe reads the referral only from that parameter, so without it no purchase through the link can be credited to the partner."},"CreateProgramRequest":{"type":"object","properties":{"name":{"type":"string","minLength":1},"commissionRule":{},"attributionWindowDays":{"type":"integer","minimum":1,"maximum":365},"attributionModel":{"type":"string","enum":["last_click","first_click","linear","position"]},"renewalsStayWithPartner":{"type":"boolean"},"couponsYieldToLinks":{"type":"boolean"},"destinationUrl":{"type":"string","format":"uri"},"deepLinkAllowedDomains":{"type":"string","maxLength":1000},"holdbackDays":{"type":"integer","minimum":0,"maximum":365},"startsAt":{"type":["string","null"],"format":"date-time","pattern":"Z$"},"endsAt":{"type":["string","null"],"format":"date-time","pattern":"Z$"},"customerReward":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["percent_off","amount_off","free_months"]},"value":{"type":"number","exclusiveMinimum":0},"currency":{"type":"string","minLength":3,"maxLength":3},"duration":{"type":"string","enum":["once","forever","repeating"]},"durationInMonths":{"type":"integer","exclusiveMinimum":0,"maximum":120}},"required":["type","value","duration"]},"shareOnNetwork":{"type":"boolean"},"marketplaceDescription":{"type":["string","null"],"maxLength":4000},"marketplaceCoverUrl":{"type":["string","null"],"maxLength":1000},"marketplaceVideoUrl":{"type":["string","null"],"maxLength":500},"categories":{"type":"array","items":{"type":"string"},"maxItems":5},"partnersMayCustomizeCode":{"type":"boolean"},"revenueEventTypes":{"type":"array","items":{"type":"string","minLength":1,"maxLength":80},"maxItems":20},"recruiterCommissionPercent":{"type":["number","null"],"exclusiveMinimum":0,"maximum":50},"recruiterCommissionMonths":{"type":["integer","null"],"minimum":1,"maximum":120},"grantToAllPartners":{"type":"boolean"}},"required":["name","commissionRule","destinationUrl"]},"UpdateProgramRequest":{"type":"object","properties":{"name":{"type":"string","minLength":1},"commissionRule":{},"attributionWindowDays":{"type":"integer","minimum":1,"maximum":365},"attributionModel":{"type":"string","enum":["last_click","first_click","linear","position"]},"renewalsStayWithPartner":{"type":"boolean"},"couponsYieldToLinks":{"type":"boolean"},"destinationUrl":{"type":"string","format":"uri"},"deepLinkAllowedDomains":{"type":["string","null"],"maxLength":1000},"holdbackDays":{"type":["integer","null"],"minimum":0,"maximum":365},"startsAt":{"type":["string","null"],"format":"date-time","pattern":"Z$"},"endsAt":{"type":["string","null"],"format":"date-time","pattern":"Z$"},"customerReward":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["percent_off","amount_off","free_months"]},"value":{"type":"number","exclusiveMinimum":0},"currency":{"type":"string","minLength":3,"maxLength":3},"duration":{"type":"string","enum":["once","forever","repeating"]},"durationInMonths":{"type":"integer","exclusiveMinimum":0,"maximum":120}},"required":["type","value","duration"]},"shareOnNetwork":{"type":"boolean"},"marketplaceDescription":{"type":["string","null"],"maxLength":4000},"marketplaceCoverUrl":{"type":["string","null"],"maxLength":1000},"marketplaceVideoUrl":{"type":["string","null"],"maxLength":500},"categories":{"type":"array","items":{"type":"string"},"maxItems":5},"partnersMayCustomizeCode":{"type":"boolean"},"revenueEventTypes":{"type":"array","items":{"type":"string","minLength":1,"maxLength":80},"maxItems":20},"recruiterCommissionPercent":{"type":["number","null"],"exclusiveMinimum":0,"maximum":50},"recruiterCommissionMonths":{"type":["integer","null"],"minimum":1,"maximum":120}}},"ProgramGrant":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"destinationUrl":{"type":"string","format":"uri"},"deepLinkAllowedDomains":{"type":["string","null"]},"commissionRule":{"$ref":"#/components/schemas/CommissionRule"},"holdbackDays":{"type":["integer","null"]},"granted":{"type":"boolean"},"grantSource":{"type":["string","null"],"enum":["admin","offering",null]},"grantedAt":{"type":["string","null"],"format":"date-time","pattern":"Z$"},"commission":{"anyOf":[{"$ref":"#/components/schemas/CommissionSnapshot"},{"type":"null"}],"description":"This partner's terms on this program (setPartnerProgramCommission). Null: the partner-wide override, or the program's own rule, applies."}},"required":["id","name","destinationUrl","deepLinkAllowedDomains","commissionRule","holdbackDays","granted","grantSource","grantedAt","commission"]},"GrantProgramsRequest":{"type":"object","properties":{"programIds":{"type":"array","items":{"type":"string","minLength":1},"minItems":1}},"required":["programIds"]},"Link":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"linkKey":{"type":"string"},"partnerId":{"type":"string"},"programId":{"type":"string"},"destinationUrl":{"type":["string","null"],"description":"Deep-link override. Null = the program's destinationUrl."},"createdAt":{"type":"string","format":"date-time","pattern":"Z$"}},"required":["id","tenantId","linkKey","partnerId","programId","destinationUrl","createdAt"],"description":"A partner share link. The public URL is built from the router base, not stored: `https://app.openpartner.dev/r/<tenantSlug>/<linkKey>` on OpenPartner Cloud, `https://<custom-domain>/r/<linkKey>` on a white-label domain, `<router-origin>/r/<linkKey>` self-hosted."},"LinkSaved":{"allOf":[{"$ref":"#/components/schemas/Link"},{"type":"object","properties":{"warnings":{"$ref":"#/components/schemas/SaveWarnings"}}}],"description":"A partner share link. The public URL is built from the router base, not stored: `https://app.openpartner.dev/r/<tenantSlug>/<linkKey>` on OpenPartner Cloud, `https://<custom-domain>/r/<linkKey>` on a white-label domain, `<router-origin>/r/<linkKey>` self-hosted."},"CreateLinkRequest":{"type":"object","properties":{"linkKey":{"type":"string","minLength":3,"maxLength":64,"pattern":"^[a-zA-Z0-9_-]+$"},"programId":{"type":"string","minLength":1},"destinationUrl":{"type":"string","format":"uri"}},"required":["linkKey","programId"]},"IdentifyResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"identityId":{"type":["string","null"]},"firstStitch":{"type":"boolean","description":"False when this (cref, userId) pair was already stitched; nothing was written."},"backfilledAttributions":{"type":"integer","description":"Events already recorded for this userId that attributed now that the click is known."}},"required":["ok","identityId","firstStitch","backfilledAttributions"]},"IdentifyRequest":{"type":"object","properties":{"cref":{"type":"string","minLength":1},"userId":{"type":"string","minLength":1},"ts":{"type":"number"}},"required":["cref","userId"]},"LandingResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"firstLanding":{"type":"boolean","description":"False when this click had already landed; nothing was written."}},"required":["ok","firstLanding"]},"LandingRequest":{"type":"object","properties":{"cref":{"type":"string","minLength":1,"maxLength":200}},"required":["cref"]},"EventResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"eventId":{"type":"string"},"attribution":{"$ref":"#/components/schemas/AttributionResult"},"replayed":{"type":"boolean","enum":[true],"description":"Present when an earlier call with the same `externalEventId` already recorded this event. `eventId` and `attribution` are that call's."}},"required":["ok","eventId","attribution"]},"AttributionResult":{"type":"object","properties":{"status":{"type":"string","enum":["attributed","no_identity","no_click","outside_window","already_attributed","not_attributed","corrective"],"description":"`outside_window`: no click is inside the window and the customer has no earlier attributed conversion to carry it (or the program has `renewalsStayWithPartner` off). A renewal of an attributed conversion attributes whatever the click's age. `not_attributed` appears only on a replay of an event that did not attribute the first time; which miss it was is not stored. `corrective`: a `refund`, `chargeback`, `dispute_created` or `invoice_payment_failed` event, recorded but never attributed. It reverses nothing; report refunds to `POST /attribution/refunds`."},"model":{"type":"string","enum":["last_click","first_click","linear","position"]},"touches":{"type":"array","items":{"type":"object","properties":{"clickId":{"type":"string"},"partnerId":{"type":"string"},"weight":{"type":"number"},"attributionId":{"type":"string"},"commissionId":{"type":"string"}},"required":["clickId","partnerId","weight","attributionId"]}},"commission":{"type":"string","enum":["accrued","no_matching_rule","none"],"description":"Present when `status` is `attributed`. `accrued`: at least one commission was written. `no_matching_rule`: no commission rule on the partner's terms takes this event type, so nothing is paid. Usually a typo'd stage name (`funded` for `loan_funded`); such types are listed under `GET /attribution/unmatched-event-types`. `none`: a rule takes the type but nothing accrued (the program has ended, a `first` rule already paid, a cap was reached, or the amount came to zero). On a replay with no commission, checked against the rules as they are now."}},"required":["status"]},"RecordEventRequest":{"type":"object","properties":{"userId":{"type":"string","minLength":1},"type":{"type":"string","minLength":1},"value":{"type":"number","minimum":0},"currency":{"type":"string","pattern":"^[A-Za-z]{3}$"},"metadata":{"type":"object","additionalProperties":{}},"ts":{"type":"string","format":"date-time","pattern":"Z$"},"externalEventId":{"anyOf":[{"type":"string","maxLength":120},{"type":"number"},{"type":"null"}],"description":"Idempotency key: your stable id for this one event, up to 120 characters. A number is used as its string. `null`, `\"\"` or a blank string mean no key."}},"required":["userId","type"]},"ConversionResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"eventId":{"type":"string"},"userId":{"type":"string","description":"The user the click was stitched to: your `userId`, or the click's own synthetic user when you sent none."},"outcome":{"type":"string","enum":["attributed","no_matching_rule","not_attributed"],"description":"What the conversion came to. `attributed`: a partner was credited (check `reason` and `attribution.commission`). `no_matching_rule`: a partner was credited, but no commission rule takes this `type`, so nothing is paid: check the stage name. `not_attributed`: no partner was credited (see `reason`). The conversion is recorded either way, so don't retry it. `GET /postback` answers the same words as text."},"reason":{"type":"string","enum":["no_commission","outside_window","no_click","no_identity","corrective"],"description":"With `attributed`: `no_commission` when a rule takes the type but nothing accrued. With `not_attributed`: `outside_window` (the click is older than the program's attribution window, or newer than the conversion's `ts`), `no_click` (the partner was revoked), `corrective` (a `refund`, `chargeback`, `dispute_created` or `invoice_payment_failed` type, which is recorded but never attributed; report refunds to `POST /attribution/refunds`). A replay of a miss carries no reason, because which miss it was isn't stored."},"attribution":{"$ref":"#/components/schemas/AttributionResult"},"replayed":{"type":"boolean","enum":[true],"description":"Present when an earlier call with the same `externalEventId` already recorded this conversion. `eventId` and `attribution` are that call's."}},"required":["ok","eventId","userId","outcome","attribution"]},"RecordConversionRequest":{"type":"object","properties":{"clickId":{"type":"string","minLength":1,"maxLength":64},"type":{"type":"string","minLength":1,"maxLength":255},"value":{"type":"number","minimum":0},"currency":{"type":"string","pattern":"^[A-Za-z]{3}$"},"externalEventId":{"anyOf":[{"type":"string","minLength":1,"maxLength":120},{"type":"number"}],"description":"Idempotency key: your stable id for this one conversion (a loan or transaction id), up to 120 characters. A number is used as its string."},"userId":{"type":"string","minLength":1,"maxLength":255},"ts":{"type":"string","format":"date-time","pattern":"Z$"},"metadata":{"type":"object","additionalProperties":{}}},"required":["clickId","type","externalEventId"]},"PostbackToken":{"type":"object","properties":{"prefix":{"type":"string","description":"The start of the token, `opcp_…`, to tell tokens apart. The full token is shown only once."},"ipAllowlist":{"type":"array","items":{"type":"string"},"description":"IP addresses or CIDR ranges `GET /postback` accepts calls from. Empty: any address."},"createdAt":{"type":"string","format":"date-time","pattern":"Z$"},"lastUsedAt":{"type":["string","null"],"format":"date-time","pattern":"Z$"}},"required":["prefix","ipAllowlist","createdAt","lastUsedAt"]},"RotatePostbackTokenRequest":{"type":"object","properties":{"ipAllowlist":{"type":"array","items":{"type":"string","maxLength":64},"maxItems":50,"example":["203.0.113.0/24"]}}},"UpdatePostbackAllowlistRequest":{"type":"object","properties":{"ipAllowlist":{"type":"array","items":{"type":"string","maxLength":64},"maxItems":50,"example":["203.0.113.0/24","198.51.100.7"]}},"required":["ipAllowlist"]},"UnmatchedEventTypes":{"type":"object","properties":{"since":{"type":"string","format":"date-time","pattern":"Z$"},"eventTypes":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string"},"events":{"type":"integer","description":"Attributed events of this type since `since` that no rule took."},"lastSeenAt":{"type":"string","format":"date-time","pattern":"Z$"},"programs":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"]}}},"required":["type","events","lastSeenAt","programs"]}}},"required":["since","eventTypes"]},"RefundResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"refundEventId":{"type":"string","description":"The corrective event recorded for this refund."},"conversionEventId":{"type":"string"},"kind":{"type":"string","enum":["refund","chargeback"]},"amount":{"anyOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}],"description":"This refund's amount, positive. Null for a refund of a conversion with no value."},"refundedAmount":{"anyOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}],"description":"Everything refunded on this conversion so far, this refund included. Null when the conversion has no value."},"conversionValue":{"anyOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}]},"currency":{"type":"string"},"fullRefund":{"type":"boolean","description":"Nothing of the conversion is left."},"actionRequired":{"type":"boolean","description":"Some commission is `held_in_transfer` (retry later) or `clawback_recorded` (the partner was already paid; usually deducted from their next payout, see `clawback_recorded`)."},"commissions":{"type":"array","items":{"$ref":"#/components/schemas/RefundCommission"},"description":"One entry per commission the conversion accrued; empty when no partner was credited."},"replayed":{"type":"boolean","enum":[true],"description":"Present when an earlier call with the same `externalEventId` recorded this refund. Nothing new is recorded; the refund is re-applied, which finishes any commission that was held in transfer, and `commissions` shows where they stand now."}},"required":["ok","refundEventId","conversionEventId","kind","amount","refundedAmount","conversionValue","currency","fullRefund","actionRequired","commissions"]},"RefundCommission":{"type":"object","properties":{"commissionId":{"type":"string","description":"The commission the conversion originally accrued."},"partnerId":{"type":"string"},"currency":{"type":"string"},"originalAmount":{"$ref":"#/components/schemas/Decimal"},"keptAmount":{"allOf":[{"$ref":"#/components/schemas/Decimal"},{"description":"What the partner keeps for this conversion now: the live commission's amount, or a paid one's amount net of adjustments."}]},"status":{"type":"string","enum":["reversed","reduced","clawback_recorded","held_in_transfer","unchanged"],"description":"`reversed`: unpaid, and nothing is kept (a full refund), so it was reversed. `reduced`: unpaid; reversed and replaced by `currentCommissionId` for the kept amount (a partial refund). `clawback_recorded`: already paid. It stays paid, and a negative adjustment of `clawbackAmount` records the refunded revenue the partner holds. OpenPartner deducts it from the partner's next manual or self-hosted Stripe Connect payout; funded Stripe Connect payouts on hosted OpenPartner don't, so recover those with the partner. `held_in_transfer`: a payout for it is in flight, so nothing changed. Retry this request (same `externalEventId`) once the payout settles. `unchanged`: nothing to take back."},"currentCommissionId":{"type":["string","null"],"description":"The live commission row: the original, its replacement, or null once reversed."},"currentStatus":{"type":["string","null"],"enum":["accrued","approved","paid",null]},"clawbackAmount":{"anyOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}],"description":"Paid commissions only: the refund clawbacks recorded against it, negative. Null when none."}},"required":["commissionId","partnerId","currency","originalAmount","keptAmount","status","currentCommissionId","currentStatus","clawbackAmount"]},"RecordRefundRequest":{"type":"object","properties":{"conversionExternalEventId":{"anyOf":[{"type":"string","minLength":1,"maxLength":120},{"type":"number"}],"description":"The `externalEventId` you sent with the conversion being refunded. Send this or `conversionEventId`, not both. A number is used as its string."},"conversionEventId":{"type":"string","minLength":1,"maxLength":64},"amount":{"type":"number","minimum":0.01,"description":"Major units. Omit to refund everything not yet refunded."},"currency":{"type":"string","minLength":3,"maxLength":3},"kind":{"type":"string","enum":["refund","chargeback"]},"reason":{"type":"string","minLength":1,"maxLength":500},"externalEventId":{"anyOf":[{"type":"string","minLength":1,"maxLength":120},{"type":"number"}],"description":"Idempotency key: your stable id for this one refund (a refund or credit-note id), up to 120 characters. A number is used as its string."},"ts":{"type":"string","format":"date-time","pattern":"Z$"},"metadata":{"type":"object","additionalProperties":{}}},"required":["externalEventId"],"oneOf":[{"required":["conversionExternalEventId"]},{"required":["conversionEventId"]}]},"ClickIngestResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"id":{"type":"string"},"replayed":{"type":"boolean","enum":[true]}},"required":["ok","id"]},"IngestClickRequest":{"type":"object","properties":{"id":{"type":"string","minLength":20,"maxLength":40},"linkKey":{"type":"string","minLength":3,"maxLength":64},"landingUrl":{"type":"string","maxLength":2048,"format":"uri"},"ipHash":{"type":["string","null"],"maxLength":64},"userAgent":{"type":["string","null"],"maxLength":500},"referer":{"type":["string","null"],"maxLength":500},"utmSource":{"type":["string","null"],"maxLength":255},"utmMedium":{"type":["string","null"],"maxLength":255},"utmCampaign":{"type":["string","null"],"maxLength":255},"utmTerm":{"type":["string","null"],"maxLength":255},"utmContent":{"type":["string","null"],"maxLength":255},"subId":{"type":["string","null"],"maxLength":255},"country":{"type":["string","null"],"minLength":2,"maxLength":2},"fraudFlag":{"type":["string","null"],"enum":["velocity","manual","revoked",null]},"ts":{"type":"string","format":"date-time","pattern":"Z$"}},"required":["id","linkKey","landingUrl","ts"]},"CouponWithStats":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"partnerId":{"type":"string"},"programId":{"type":"string"},"code":{"type":"string"},"stripeCouponId":{"type":["string","null"]},"stripePromotionCodeId":{"type":["string","null"]},"brandChosen":{"type":"boolean","description":"The brand's admin typed this code. Only then does a Stripe promotion code or coupon the brand already had under the same string credit the partner."},"deactivatedAt":{"type":["string","null"],"format":"date-time","pattern":"Z$"},"createdAt":{"type":"string","format":"date-time","pattern":"Z$"},"canEdit":{"type":"boolean","description":"The program lets partners rename their own codes."},"redemptions90d":{"type":"integer"},"revenue90d":{"type":"number"}},"required":["id","tenantId","partnerId","programId","code","stripeCouponId","stripePromotionCodeId","brandChosen","deactivatedAt","createdAt","canEdit","redemptions90d","revenue90d"]},"Coupon":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"partnerId":{"type":"string"},"programId":{"type":"string"},"code":{"type":"string"},"stripeCouponId":{"type":["string","null"]},"stripePromotionCodeId":{"type":["string","null"]},"brandChosen":{"type":"boolean","description":"The brand's admin typed this code. Only then does a Stripe promotion code or coupon the brand already had under the same string credit the partner."},"deactivatedAt":{"type":["string","null"],"format":"date-time","pattern":"Z$"},"createdAt":{"type":"string","format":"date-time","pattern":"Z$"},"canEdit":{"type":"boolean","description":"The program lets partners rename their own codes."}},"required":["id","tenantId","partnerId","programId","code","stripeCouponId","stripePromotionCodeId","brandChosen","deactivatedAt","createdAt","canEdit"]},"CreateCouponRequest":{"type":"object","properties":{"programId":{"type":"string","minLength":1},"code":{"type":"string","minLength":3,"maxLength":40,"pattern":"^[A-Z0-9-]+$"}},"required":["programId"]},"CouponRedeemResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"eventId":{"type":"string"},"partnerId":{"type":"string","description":"Omitted on a replay."},"replayed":{"type":"boolean","enum":[true]}},"required":["ok","eventId"]},"RedeemCouponRequest":{"type":"object","properties":{"code":{"type":"string","minLength":3,"maxLength":40},"eventType":{"type":"string","minLength":1,"maxLength":80},"value":{"type":"number","minimum":0},"currency":{"type":"string","pattern":"^[A-Za-z]{3}$"},"externalEventId":{"type":"string","minLength":1,"maxLength":120},"userId":{"type":"string","minLength":1,"maxLength":120},"ts":{"type":"string","format":"date-time","pattern":"Z$"}},"required":["code","eventType","externalEventId","userId"]},"CommissionPage":{"type":"object","properties":{"commissions":{"type":"array","items":{"$ref":"#/components/schemas/CommissionListItem"}},"nextCursor":{"type":["string","null"]}},"required":["commissions","nextCursor"]},"CommissionListItem":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"attributionId":{"type":["string","null"],"description":"The attributed sale this commission was earned on. Null on a bonus's commission (`bonusId` is set instead)."},"bonusId":{"type":["string","null"],"description":"The bonus this commission pays (`POST /bonuses`). Null on a sale's commission."},"sourceCommissionId":{"type":["string","null"],"description":"Set on a recruiter commission: the recruited partner's commission this one is a share of. It is on the same `attributionId` (the recruit's sale), and `partnerId` is the recruiter. Null on every other commission."},"partnerId":{"type":"string"},"amount":{"$ref":"#/components/schemas/Decimal"},"currency":{"type":"string"},"status":{"type":"string","enum":["accrued","approved","paid","reversed"],"description":"`paid` means the commission is in a payout. On the manual rail that happens when the payout is planned, before the money is sent: check the payout's own `status`."},"accruedAt":{"type":"string","format":"date-time","pattern":"Z$"},"paidAt":{"type":["string","null"],"format":"date-time","pattern":"Z$"},"payoutId":{"type":["string","null"]},"metadata":{"type":"object","additionalProperties":{},"description":"How the amount was worked out. `rateType` is the sub-rule's `percent` or `fixed` (absent on commissions accrued before October 2026). A banded or tiered rule also records `slab`: which sub-rule (`ruleIndex`), `kind` (`band` or `tier`), the `level` reached and its `threshold` and `value`, and for tiers the `metric` and its `metricValue` over the `window`. A replacement accrued for what a partial refund left records `refund`. A recruiter commission has `rateType: \"recruiter\"` and `recruiter`: the `percent` it was worked out at and the `recruitedPartnerId`."},"partnerName":{"type":["string","null"],"description":"The partner's name. Null only if the partner record is gone."},"holdbackDays":{"type":["integer","null"]},"programId":{"type":["string","null"]},"campaignName":{"type":["string","null"],"description":"The program's name (kept under its pre-rename key)."},"matureAt":{"type":["string","null"],"format":"date-time","description":"When the holdback ends and the commission becomes approvable. Null = no holdback.","pattern":"Z$"},"source":{"type":"string","enum":["attribution","bonus"],"description":"`attribution`: earned on an attributed sale. `bonus`: pays a bonus, with no event or click behind it."},"eventType":{"type":["string","null"],"description":"The type of the attributed event the commission was earned on: `signup`, `trial_started`, `subscription_created`, `invoice_paid`, or one of your custom types. Null on a bonus's commission."},"bonus":{"type":["object","null"],"properties":{"id":{"type":"string"},"source":{"type":"string","enum":["manual","performance_bounty","network"]},"note":{"type":["string","null"],"description":"Shown to the partner."},"bountyId":{"type":["string","null"]},"bountyName":{"type":["string","null"],"description":"The performance bounty's name, when a bounty paid it."}},"required":["id","source","note","bountyId","bountyName"],"description":"The bonus behind a `source: \"bonus\"` commission; null otherwise."},"recruiter":{"type":["object","null"],"properties":{"sourceCommissionId":{"type":"string"},"recruitedPartnerId":{"type":["string","null"]},"recruitedPartnerName":{"type":["string","null"]},"percent":{"type":["number","null"],"description":"The share it was worked out at, as a percent."}},"required":["sourceCommissionId","recruitedPartnerId","recruitedPartnerName","percent"],"description":"A recruiter commission's recruit and share; null otherwise. Its `source` is `attribution` (it was earned on the recruit's sale) and its `matureAt` is null: it is approved with the recruit's commission, not on a holdback of its own."}},"required":["id","tenantId","attributionId","bonusId","sourceCommissionId","partnerId","amount","currency","status","accruedAt","paidAt","payoutId","metadata","partnerName","holdbackDays","programId","campaignName","matureAt","source","eventType","bonus","recruiter"]},"Commission":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"attributionId":{"type":["string","null"],"description":"The attributed sale this commission was earned on. Null on a bonus's commission (`bonusId` is set instead)."},"bonusId":{"type":["string","null"],"description":"The bonus this commission pays (`POST /bonuses`). Null on a sale's commission."},"sourceCommissionId":{"type":["string","null"],"description":"Set on a recruiter commission: the recruited partner's commission this one is a share of. It is on the same `attributionId` (the recruit's sale), and `partnerId` is the recruiter. Null on every other commission."},"partnerId":{"type":"string"},"amount":{"$ref":"#/components/schemas/Decimal"},"currency":{"type":"string"},"status":{"type":"string","enum":["accrued","approved","paid","reversed"],"description":"`paid` means the commission is in a payout. On the manual rail that happens when the payout is planned, before the money is sent: check the payout's own `status`."},"accruedAt":{"type":"string","format":"date-time","pattern":"Z$"},"paidAt":{"type":["string","null"],"format":"date-time","pattern":"Z$"},"payoutId":{"type":["string","null"]},"metadata":{"type":"object","additionalProperties":{},"description":"How the amount was worked out. `rateType` is the sub-rule's `percent` or `fixed` (absent on commissions accrued before October 2026). A banded or tiered rule also records `slab`: which sub-rule (`ruleIndex`), `kind` (`band` or `tier`), the `level` reached and its `threshold` and `value`, and for tiers the `metric` and its `metricValue` over the `window`. A replacement accrued for what a partial refund left records `refund`. A recruiter commission has `rateType: \"recruiter\"` and `recruiter`: the `percent` it was worked out at and the `recruitedPartnerId`."}},"required":["id","tenantId","attributionId","bonusId","sourceCommissionId","partnerId","amount","currency","status","accruedAt","paidAt","payoutId","metadata"]},"BonusAwarded":{"type":"object","properties":{"replayed":{"type":"boolean","enum":[true],"description":"Set on a replay of an `externalRef` already used: the original bonus."},"bonus":{"$ref":"#/components/schemas/Bonus"},"commission":{"$ref":"#/components/schemas/Commission"}},"required":["bonus","commission"]},"Bonus":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"partnerId":{"type":"string"},"programId":{"type":["string","null"]},"amount":{"$ref":"#/components/schemas/Decimal"},"currency":{"type":"string"},"source":{"type":"string","enum":["manual","performance_bounty","network"],"description":"`manual`: paid with `POST /bonuses` (an admin or your integration). `network`: an OpenPartner Network bounty, paid with the Network's federation key. `performance_bounty`: the partner reached a bounty's goal. New values may be added."},"bountyId":{"type":["string","null"]},"externalRef":{"type":["string","null"]},"note":{"type":["string","null"]},"createdBy":{"type":["string","null"],"description":"The admin or API key that paid it; null for a performance bounty."},"createdAt":{"type":"string","format":"date-time","pattern":"Z$"}},"required":["id","tenantId","partnerId","programId","amount","currency","source","bountyId","externalRef","note","createdBy","createdAt"]},"AwardBonusRequest":{"type":"object","properties":{"partnerId":{"type":"string","minLength":1,"maxLength":64},"programId":{"type":"string","minLength":1,"maxLength":64},"amount":{"type":"number","exclusiveMinimum":0,"maximum":999999999.99},"currency":{"type":"string","pattern":"^[A-Za-z]{3}$"},"note":{"type":"string","minLength":1,"maxLength":1000},"externalRef":{"type":"string","minLength":1,"maxLength":200},"hold":{"type":"boolean"}},"required":["partnerId","amount","currency"]},"BonusPage":{"type":"object","properties":{"bonuses":{"type":"array","items":{"$ref":"#/components/schemas/BonusListItem"}},"nextCursor":{"type":["string","null"]}},"required":["bonuses","nextCursor"]},"BonusListItem":{"allOf":[{"$ref":"#/components/schemas/Bonus"},{"type":"object","properties":{"commission":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","enum":["accrued","approved","paid","reversed"]},"payoutId":{"type":["string","null"]},"paidAt":{"type":["string","null"],"format":"date-time","pattern":"Z$"}},"required":["id","status","payoutId","paidAt"]}},"required":["commission"]}]},"PayoutRunResult":{"type":"object","properties":{"runId":{"type":"string"},"mode":{"type":"string","enum":["selfhost","flat","revshare"]},"skippedBelowThreshold":{"type":"array","items":{"type":"object","properties":{"partnerId":{"type":"string"},"partnerName":{"type":["string","null"],"description":"The partner's name. Null only if the partner record is gone."},"currency":{"type":"string"},"amount":{"type":"number"}},"required":["partnerId","partnerName","currency","amount"]}},"skippedUnfunded":{"type":"array","items":{"type":"object","properties":{"partnerId":{"type":"string"},"partnerName":{"type":["string","null"],"description":"The partner's name. Null only if the partner record is gone."},"currency":{"type":"string"},"amount":{"type":"number"}},"required":["partnerId","partnerName","currency","amount"]}},"heldForClawback":{"type":"array","items":{"type":"object","properties":{"partnerId":{"type":"string"},"partnerName":{"type":["string","null"],"description":"The partner's name. Null only if the partner record is gone."},"currency":{"type":"string"},"amount":{"type":"number"},"clawbackAmount":{"type":"number"}},"required":["partnerId","partnerName","currency","amount","clawbackAmount"]},"description":"Partners not paid this run because their outstanding refund clawbacks are at least what they are owed (`amount`, before clawbacks). Their commissions stay approved and are paid, net, once enough accrues."},"heldForCompliance":{"type":"array","items":{"type":"object","properties":{"partnerId":{"type":"string"},"partnerName":{"type":["string","null"],"description":"The partner's name. Null only if the partner record is gone."},"currency":{"type":"string"},"amount":{"type":"number"},"reason":{"type":"string","enum":["country_not_supported","screening_confirmed","screening_review","screening_pending"],"description":"Why the partner is on hold: `country_not_supported` (their declared country isn't served), `screening_review` (a possible sanctions-list match waits for OpenPartner's review), `screening_confirmed` (a confirmed match), or `screening_pending` (not screened yet; usually clears within the hour)."}},"required":["partnerId","partnerName","currency","amount","reason"]},"description":"OpenPartner Cloud: Stripe payouts not made this run because the partner is on a compliance hold (`GET /payouts/holds`). Their commissions stay approved and are paid on the first run after the hold lifts. `amount` is what they are owed."},"payouts":{"type":"array","items":{"type":"object","properties":{"payoutId":{"type":"string"},"partnerId":{"type":"string"},"partnerName":{"type":["string","null"],"description":"The partner's name. Null only if the partner record is gone."},"amount":{"type":"number"},"currency":{"type":"string"},"method":{"type":"string","enum":["stripe_connect","manual","external"]},"status":{"type":"string","enum":["pending","paid","failed"]},"platformFee":{"type":"number"},"clawbackAmount":{"type":"number","description":"Refund clawbacks netted off this payout. `amount` is already net of them."},"complianceWarning":{"type":"string","enum":["country_not_supported","screening_confirmed","screening_review","screening_pending"],"description":"A `manual` payout to a partner on a compliance hold. You send this money yourself, so the payout was still made: check before you send it."},"error":{"type":"string"}},"required":["payoutId","partnerId","partnerName","amount","currency","method","status"]}},"transfers":{"type":"object","properties":{"processed":{"type":"integer"},"confirmed":{"type":"array","items":{"type":"object","properties":{"payoutId":{"type":"string"},"stripeTransferId":{"type":"string"}},"required":["payoutId","stripeTransferId"]}},"failed":{"type":"array","items":{"type":"object","properties":{"payoutId":{"type":"string"},"error":{"type":"string"}},"required":["payoutId","error"]}},"ambiguous":{"type":"array","items":{"type":"string"}},"reconciled":{"type":"array","items":{"type":"string"}},"canceled":{"type":"array","items":{"type":"object","properties":{"payoutId":{"type":"string"},"reason":{"type":"string"}},"required":["payoutId","reason"]}},"skipped":{"type":"integer"}},"required":["processed","confirmed","failed","ambiguous","reconciled","canceled","skipped"]}},"required":["runId","mode","skippedBelowThreshold","skippedUnfunded","heldForClawback","heldForCompliance","payouts","transfers"]},"PayoutPage":{"type":"object","properties":{"payouts":{"type":"array","items":{"$ref":"#/components/schemas/PayoutListItem"}},"nextCursor":{"type":["string","null"]}},"required":["payouts","nextCursor"]},"PayoutListItem":{"allOf":[{"$ref":"#/components/schemas/Payout"},{"type":"object","properties":{"partnerName":{"type":["string","null"],"description":"The partner's name. Null only if the partner record is gone."},"sent":{"anyOf":[{"$ref":"#/components/schemas/PayoutSent"},{"type":"null"}],"description":"Set while (or once) a PayPal batch sends this payout in another currency; gone if PayPal returns it."}},"required":["partnerName","sent"]}]},"PayoutSent":{"type":"object","properties":{"amount":{"allOf":[{"$ref":"#/components/schemas/Decimal"},{"description":"What PayPal was asked to send."}]},"currency":{"type":"string"},"rate":{"type":"string","pattern":"^\\d+(\\.\\d+)?$","description":"Units of the sent currency per unit of the payout’s currency, as a decimal string (ECB reference rates, crossed through EUR).","example":"0.011958904110"},"rateSource":{"type":"string","enum":["ecb"]},"rateDate":{"type":"string","description":"The ECB reference date of the rate (YYYY-MM-DD)."}},"required":["amount","currency","rate","rateSource","rateDate"],"description":"A payout PayPal can’t send in its own currency (INR), sent through Pay with PayPal in another (USD) at the ECB reference rate frozen when the batch was made. `amount` and `currency` on the payout stay its own. PayPal’s own conversion of it into the partner’s currency is between the partner and PayPal."},"Payout":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"partnerId":{"type":"string"},"amount":{"$ref":"#/components/schemas/Decimal"},"currency":{"type":"string"},"method":{"type":"string","enum":["stripe_connect","manual","external"]},"stripeTransferId":{"type":["string","null"]},"status":{"type":"string","enum":["pending","paid","failed","partially_reversed","reversed"],"description":"A `manual` payout stays `pending` until you send the money and confirm it; only `paid` means it went out."},"metadata":{"type":"object","additionalProperties":{},"description":"What the run and the payout rails recorded. `externalReference` is the payment reference you gave when you confirmed a manual payout (`POST /payouts/{id}/confirm`)."},"createdAt":{"type":"string","format":"date-time","pattern":"Z$"},"completedAt":{"type":["string","null"],"format":"date-time","pattern":"Z$"}},"required":["id","tenantId","partnerId","amount","currency","method","stripeTransferId","status","metadata","createdAt","completedAt"]},"PayoutHolds":{"type":"object","properties":{"holds":{"type":"array","items":{"type":"object","properties":{"partnerId":{"type":"string"},"partnerName":{"type":"string"},"partnerEmail":{"type":"string"},"reason":{"type":"string","enum":["country_not_supported","screening_confirmed","screening_review","screening_pending"],"description":"Why the partner is on hold: `country_not_supported` (their declared country isn't served), `screening_review` (a possible sanctions-list match waits for OpenPartner's review), `screening_confirmed` (a confirmed match), or `screening_pending` (not screened yet; usually clears within the hour)."},"effect":{"type":"string","enum":["held","warning"],"description":"`held`: Stripe payouts are not made. `warning`: you pay this partner yourself, so nothing is held."},"owed":{"type":"array","items":{"type":"object","properties":{"currency":{"type":"string"},"amount":{"type":"number"}},"required":["currency","amount"]},"description":"Approved commissions not yet on a payout, per currency."}},"required":["partnerId","partnerName","partnerEmail","reason","effect","owed"]}}},"required":["holds"]},"ConfirmManualPayoutRequest":{"type":"object","properties":{"reference":{"type":"string","maxLength":100,"pattern":"^[^\\p{C}\\p{Zl}\\p{Zp}]*$"}}},"PayoutWithSent":{"allOf":[{"$ref":"#/components/schemas/Payout"},{"type":"object","properties":{"sent":{"anyOf":[{"$ref":"#/components/schemas/PayoutSent"},{"type":"null"}],"description":"Set while (or once) a PayPal batch sends this payout in another currency; gone if PayPal returns it."}},"required":["sent"]}]},"PayoutStatement":{"type":"object","properties":{"statementNumber":{"type":"string","description":"Your `numberPrefix` and the payout id: unique, and the same every time."},"generatedAt":{"type":"string","format":"date-time","pattern":"Z$"},"issuer":{"type":"object","properties":{"name":{"type":"string"},"tradingName":{"type":["string","null"],"description":"The program name, when it differs from `name`."},"address":{"type":["string","null"]},"gstin":{"type":["string","null"],"description":"OpenPartner Cloud: the GSTIN on your billing profile."},"logoUrl":{"type":["string","null"]}},"required":["name","tradingName","address","gstin","logoUrl"]},"partner":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":["string","null"]},"email":{"type":["string","null"]},"destination":{"type":["string","null"],"description":"Where the payout goes, masked, never in full: the last four characters of the Stripe account the transfer was made to, or the partner's payout details on file (`PayPal j•••@example.com`, `Bank account •••• 1234 (INR)`). Null when a manual payout's partner has no details on file."},"destinationChanged":{"type":"boolean","description":"The partner changed their payout details after the payout was sent (or put in a batch file); `destination` shows the current ones."}},"required":["id","name","email","destination","destinationChanged"]},"payout":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","enum":["pending","paid","failed","partially_reversed","reversed"]},"method":{"type":"string","enum":["stripe_connect","manual","external"]},"methodLabel":{"type":"string","example":"Manual · Wise batch payment"},"reference":{"type":["object","null"],"properties":{"label":{"type":"string"},"value":{"type":"string"}},"required":["label","value"],"description":"What identifies the payout: the Stripe transfer (`Stripe transfer`), its PayPal Customer ID or Wise reference in a batch file (`PayPal Customer ID`, `Wise payment reference`), or, for a payout sent one at a time, the `reference` you gave `POST /payouts/{id}/confirm` (`Payment reference`). Null when there is none."},"amount":{"allOf":[{"$ref":"#/components/schemas/Decimal"},{"description":"What was paid, or is to be."}]},"currency":{"type":"string"},"sent":{"type":["object","null"],"properties":{"amount":{"$ref":"#/components/schemas/Decimal"},"currency":{"type":"string"},"rate":{"allOf":[{"$ref":"#/components/schemas/Decimal"},{"description":"Units of `currency` per unit of the payout currency."}]},"rateSource":{"type":"string","enum":["ecb"]},"rateDate":{"type":"string","example":"2026-10-22"}},"required":["amount","currency","rate","rateSource","rateDate"],"description":"When Pay with PayPal sent the payout in another currency (an INR balance in USD), what was sent and at which ECB reference rate. `amount` and the line items stay in the payout currency. Null otherwise."},"createdAt":{"type":"string","format":"date-time","pattern":"Z$"},"completedAt":{"type":["string","null"],"format":"date-time","pattern":"Z$"}},"required":["id","status","method","methodLabel","reference","amount","currency","sent","createdAt","completedAt"]},"commissions":{"type":"array","items":{"$ref":"#/components/schemas/PayoutStatementLine"}},"bonuses":{"type":"array","items":{"$ref":"#/components/schemas/PayoutStatementLine"}},"clawbacks":{"type":"array","items":{"$ref":"#/components/schemas/PayoutStatementLine"},"description":"Refund clawbacks deducted from this payout."},"reversals":{"type":"array","items":{"$ref":"#/components/schemas/PayoutStatementLine"},"description":"Stripe transfer reversals after the payout was paid. Not in `totals`."},"taxLines":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string"},"amount":{"allOf":[{"$ref":"#/components/schemas/Decimal"},{"type":["string","null"]}]},"currency":{"type":"string"}},"required":["label","amount","currency"]},"description":"Empty unless you turned tax lines on (`POST /config/payout-statements`). `amount` is always null for now."},"totals":{"type":"array","items":{"type":"object","properties":{"currency":{"type":"string"},"commissions":{"$ref":"#/components/schemas/Decimal"},"bonuses":{"$ref":"#/components/schemas/Decimal"},"clawbacks":{"$ref":"#/components/schemas/Decimal"},"net":{"$ref":"#/components/schemas/Decimal"}},"required":["currency","commissions","bonuses","clawbacks","net"]},"description":"One per currency the lines are in, never added across currencies. A payout has one; `net` matches `payout.amount` except where the currency is paid in whole units."},"notes":{"type":"array","items":{"type":"string"},"description":"Plain sentences: a pending, failed or reversed payout, or rounding."},"footer":{"type":["string","null"]}},"required":["statementNumber","generatedAt","issuer","partner","payout","commissions","bonuses","clawbacks","reversals","taxLines","totals","notes","footer"]},"PayoutStatementLine":{"type":"object","properties":{"id":{"type":"string","description":"The commission, bonus, commission adjustment or reversal id."},"commissionId":{"type":["string","null"],"description":"The commission behind the line; null on a reversal."},"date":{"type":"string","description":"YYYY-MM-DD (UTC): when it was earned, or when a deduction or reversal was recorded.","example":"2026-10-14"},"programName":{"type":["string","null"]},"description":{"type":"string","example":"Invoice paid"},"amount":{"allOf":[{"$ref":"#/components/schemas/Decimal"},{"description":"Signed: deductions and reversals are negative."}]},"currency":{"type":"string"}},"required":["id","commissionId","date","programName","description","amount","currency"]},"BulkConfirmResult":{"type":"object","properties":{"confirmed":{"type":"array","items":{"$ref":"#/components/schemas/Payout"}},"notConfirmable":{"type":"array","items":{"type":"object","properties":{"payoutId":{"type":"string"},"error":{"type":"string","enum":["payout_not_found","not_confirmable"]},"reason":{"anyOf":[{"type":"string","enum":["country_not_supported","screening_confirmed"],"description":"OpenPartner Cloud: the payout must not be paid. `country_not_supported`: the partner, or their payout account, is in a country OpenPartner doesn't serve. `screening_confirmed`: a confirmed sanctions-list match. Every confirm refuses it."},{"type":"string","enum":["in_paypal_batch"]}],"description":"A do-not-pay hold, or `in_paypal_batch`: PayPal reports whether this payout was paid."},"detail":{"type":"string"}},"required":["payoutId","error"]}}},"required":["confirmed","notConfirmable"]},"ManualPayoutRuns":{"type":"object","properties":{"runs":{"type":"array","items":{"type":"object","properties":{"runId":{"type":"string"},"createdAt":{"type":"string","format":"date-time","pattern":"Z$"},"pendingCount":{"type":"integer","description":"Manual payouts in the run not yet confirmed as sent."},"pendingTotals":{"type":"array","items":{"type":"object","properties":{"currency":{"type":"string"},"amount":{"$ref":"#/components/schemas/Decimal"}},"required":["currency","amount"]}}},"required":["runId","createdAt","pendingCount","pendingTotals"]}},"nextCursor":{"type":["string","null"],"description":"Pass as `cursor` for the next page; null on the last."}},"required":["runs","nextCursor"]},"ManualPayoutRun":{"type":"object","properties":{"runId":{"type":"string"},"createdAt":{"type":"string","format":"date-time","pattern":"Z$"},"pendingCount":{"type":"integer"},"ready":{"type":"array","items":{"type":"object","properties":{"provider":{"type":"string","enum":["paypal","wise"]},"currency":{"type":"string"},"sendAs":{"type":["string","null"],"description":"Balances PayPal can’t send in `currency` (INR) that Pay with PayPal sends in this currency instead (the `paypal_payouts` setting): quote with `POST /payouts/runs/{runId}/paypal-batches/quote`, then send. No file takes them. Null for every other group."},"count":{"type":"integer"},"total":{"allOf":[{"$ref":"#/components/schemas/Decimal"},{"description":"In `currency`."}]}},"required":["provider","currency","sendAs","count","total"]},"description":"Payouts no file holds yet that a new file would take, per provider and currency (PayPal: the payout currency; Wise: the source currency). Each new file takes up to 5,000 (PayPal) or 1,000 (Wise) of them."},"exports":{"type":"array","items":{"type":"object","properties":{"exportId":{"type":"string"},"provider":{"type":"string","enum":["paypal","wise"]},"currency":{"type":"string"},"createdAt":{"type":"string","format":"date-time","pattern":"Z$"},"createdBy":{"type":"string","enum":["admin","api"]},"pendingCount":{"type":"integer","description":"Payouts in the file not confirmed yet."},"pendingTotal":{"$ref":"#/components/schemas/Decimal"},"changedCount":{"type":"integer","description":"Of those, whose partner changed or removed their payout details after the file was made. A re-download carries the new details, or leaves the payout out when they no longer fit the file."},"doNotPayCount":{"type":"integer","description":"Of those, now on a do-not-pay hold. Confirming the file refuses them."}},"required":["exportId","provider","currency","createdAt","createdBy","pendingCount","pendingTotal","changedCount","doNotPayCount"]},"description":"Files made from this run that still hold payouts to confirm."},"inPaypalBatches":{"type":"integer","description":"Pending payouts in a PayPal Payouts batch OpenPartner sent with your PayPal app. List the batches with `GET /payouts/runs/{runId}/paypal-batches`."},"excluded":{"type":"array","items":{"type":"object","properties":{"reason":{"type":"string","enum":["no_payout_details","details_unreadable","pay_individually","currency_not_supported","details_incomplete","over_paypal_limit","country_not_supported","screening_confirmed"]},"count":{"type":"integer"}},"required":["reason","count"]},"description":"Pending payouts in no file, per reason (see ManualRunPayout.excluded)."}},"required":["runId","createdAt","pendingCount","ready","exports","inPaypalBatches","excluded"]},"ManualRunPayoutPage":{"type":"object","properties":{"payouts":{"type":"array","items":{"$ref":"#/components/schemas/ManualRunPayout"}},"nextCursor":{"type":["string","null"],"description":"Pass as `cursor` for the next page; null on the last."}},"required":["payouts","nextCursor"]},"ManualRunPayout":{"type":"object","properties":{"payoutId":{"type":"string"},"partnerId":{"type":"string"},"partnerName":{"type":"string"},"partnerEmail":{"type":"string"},"amount":{"$ref":"#/components/schemas/Decimal"},"currency":{"type":"string"},"createdAt":{"type":"string","format":"date-time","pattern":"Z$"},"destination":{"anyOf":[{"type":"object","properties":{"method":{"type":"string","enum":["paypal","bank","upi","other"]},"country":{"type":["string","null"]},"summary":{"type":["string","null"],"description":"Masked. Null when the details can't be read."},"updatedAt":{"type":"string","format":"date-time","description":"A change shortly before a payout is worth a second look before you send it.","pattern":"Z$"},"updatedBy":{"type":"string","enum":["partner","admin","api"],"description":"Who saved them last: the partner (portal), an admin (portal) or an API key."}},"required":["method","country","summary","updatedAt","updatedBy"]},{"type":"null"}],"description":"The partner’s payout details, masked; null when they have none."},"export":{"anyOf":[{"type":"object","properties":{"exportId":{"type":"string"},"provider":{"type":"string","enum":["paypal","wise"]},"via":{"type":"string","enum":["file","api"],"description":"`file`: a batch file you upload. `api`: a PayPal Payouts batch OpenPartner sent with your PayPal app; `exportId` is the batch id and `paypal` what PayPal last said."},"createdAt":{"type":"string","format":"date-time","pattern":"Z$"},"reference":{"type":["string","null"],"description":"What identifies this payout in the file: the PayPal Customer ID (the payout id), or the Wise payment reference (its last 10 characters; none for JPY or COP)."},"destinationChanged":{"type":"boolean","description":"The partner changed or removed their payout details after the file was made."},"paypal":{"anyOf":[{"$ref":"#/components/schemas/PaypalItemStatus"},{"type":"null"}],"description":"A PayPal Payouts batch: this payout’s item, once PayPal has listed it."},"conversion":{"anyOf":[{"$ref":"#/components/schemas/PaypalConversion"},{"type":"null"}],"description":"A PayPal Payouts batch sending this payout in another currency (an INR balance sent in USD): its amounts and the rate, frozen when the batch was made."}},"required":["exportId","provider","via","createdAt","reference","destinationChanged","paypal","conversion"]},{"type":"null"}],"description":"The file (or PayPal Payouts batch) this payout is in. It stays there, and only there, until it is confirmed, the file is discarded, or PayPal reports it failed or returned."},"ready":{"type":["string","null"],"enum":["paypal","wise",null],"description":"The file a new export would put this payout in."},"sendAs":{"type":["string","null"],"description":"Ready for a Pay with PayPal batch only, sent in this currency at a quoted rate (a balance PayPal can’t send in its own). No file takes it."},"excluded":{"type":["string","null"],"enum":["no_payout_details","details_unreadable","pay_individually","currency_not_supported","details_incomplete","over_paypal_limit","country_not_supported","screening_confirmed",null],"description":"Why a payout is in no file: `no_payout_details`, `details_unreadable`, `pay_individually` (a UPI ID or instructions: pay it yourself), `currency_not_supported` (PayPal can't send INR; Wise can't send from this currency, or to INR yet), `details_incomplete` (Wise needs the holder's address for a payout sent in USD or AUD), `over_paypal_limit` (more than PayPal sends in one payment in this currency, e.g. USD 60,000), and on OpenPartner Cloud the do-not-pay reasons `country_not_supported` and `screening_confirmed`."},"complianceWarning":{"type":["string","null"],"enum":["country_not_supported","screening_confirmed","screening_review","screening_pending",null],"description":"OpenPartner Cloud: the partner is on a compliance hold. A sanctions review or pending screening stays payable; check before you send it."},"doNotPay":{"type":["string","null"],"enum":["country_not_supported","screening_confirmed",null],"description":"OpenPartner Cloud: the payout must not be paid. `country_not_supported`: the partner, or their payout account, is in a country OpenPartner doesn't serve. `screening_confirmed`: a confirmed sanctions-list match. Every confirm refuses it."}},"required":["payoutId","partnerId","partnerName","partnerEmail","amount","currency","createdAt","destination","export","ready","sendAs","excluded","complianceWarning","doNotPay"]},"PaypalItemStatus":{"type":"object","properties":{"itemId":{"type":"string","description":"PayPal’s payout_item_id."},"status":{"type":"string","description":"PayPal’s transaction_status: SUCCESS, PENDING, UNCLAIMED, ONHOLD, FAILED, RETURNED, BLOCKED, REFUNDED, REVERSED."},"transactionId":{"type":["string","null"]},"amount":{"allOf":[{"$ref":"#/components/schemas/Decimal"},{"type":["string","null"],"description":"What PayPal sent: whole units where PayPal takes no decimals (HUF, JPY, TWD)."}]},"currency":{"type":["string","null"]},"error":{"type":["string","null"],"description":"PayPal’s error name for an item that didn’t go through (RECEIVER_UNREGISTERED, …)."}},"required":["itemId","status","transactionId","amount","currency","error"]},"PaypalConversion":{"type":"object","properties":{"sourceAmount":{"allOf":[{"$ref":"#/components/schemas/Decimal"},{"description":"The payout’s own amount."}]},"sourceCurrency":{"type":"string"},"sentAmount":{"allOf":[{"$ref":"#/components/schemas/Decimal"},{"description":"What PayPal was asked to send: sourceAmount × rate, rounded to the cent on its own."}]},"sentCurrency":{"type":"string"},"rate":{"type":"string","pattern":"^\\d+(\\.\\d+)?$","description":"Units of the sent currency per unit of the payout’s currency, as a decimal string (ECB reference rates, crossed through EUR).","example":"0.011958904110"},"rateSource":{"type":"string","enum":["ecb"]},"rateDate":{"type":"string","description":"The ECB reference date of the rate (YYYY-MM-DD)."}},"required":["sourceAmount","sourceCurrency","sentAmount","sentCurrency","rate","rateSource","rateDate"],"description":"Stamped on a payout a PayPal batch sends in another currency (`metadata.manualExport.conversion`); exported with the payout."},"ManualExportFile":{"type":"object","properties":{"export":{"type":"object","properties":{"exportId":{"type":"string"},"runId":{"type":"string"},"provider":{"type":"string","enum":["paypal","wise"]},"currency":{"type":"string"},"createdAt":{"type":"string","format":"date-time","pattern":"Z$"},"createdBy":{"type":"string","enum":["admin","api"]},"payoutIds":{"type":"array","items":{"type":"string"},"description":"Exactly the payouts in the file, and what `POST /payouts/exports/{exportId}/confirm` confirms."},"count":{"type":"integer"},"total":{"$ref":"#/components/schemas/Decimal"}},"required":["exportId","runId","provider","currency","createdAt","createdBy","payoutIds","count","total"]},"filename":{"type":"string"},"csv":{"type":"string","description":"The file to upload to your PayPal or Wise account (UTF-8, CRLF line endings)."}},"required":["export","filename","csv"]},"PaypalConnection":{"type":"object","properties":{"id":{"type":"string"},"environment":{"type":"string","enum":["live","sandbox"]},"clientIdPreview":{"type":"string","description":"The client id’s first 6 and last 4 characters. The secret is never returned."},"appId":{"type":["string","null"],"description":"PayPal’s id for the app, from the token call."},"payoutsEnabled":{"type":"boolean","description":"PayPal granted the app the Payouts scope. False: PayPal hasn’t enabled Payouts on the account yet, and sends would be refused."},"webhook":{"type":"object","properties":{"registered":{"type":"boolean","description":"OpenPartner registered a webhook on your app, so status arrives as PayPal sends it. Otherwise batches are polled every few minutes."},"error":{"type":["string","null"]}},"required":["registered","error"]},"active":{"type":"boolean","description":"Pay with PayPal sends with this environment."},"connectedAt":{"type":"string","format":"date-time","pattern":"Z$"},"verifiedAt":{"type":["string","null"],"format":"date-time","pattern":"Z$"},"lastError":{"type":["string","null"]},"lastErrorAt":{"type":["string","null"],"format":"date-time","pattern":"Z$"},"openBatches":{"type":"integer","description":"Batches sent with this app that are still in flight. Disconnecting waits for them."}},"required":["id","environment","clientIdPreview","appId","payoutsEnabled","webhook","active","connectedAt","verifiedAt","lastError","lastErrorAt","openBatches"]},"PaypalBatch":{"type":"object","properties":{"batchId":{"type":"string","description":"Also the `sender_batch_id` PayPal shows, and each payout’s `exportId`."},"runId":{"type":"string"},"environment":{"type":"string","enum":["live","sandbox"]},"currency":{"type":"string","description":"What PayPal sends: the payouts’ currency, or the one their balances were converted to (`conversion`)."},"itemCount":{"type":"integer"},"total":{"allOf":[{"$ref":"#/components/schemas/Decimal"},{"description":"In `currency`: what PayPal is asked to send."}]},"conversion":{"anyOf":[{"type":"object","properties":{"sourceCurrency":{"type":"string","description":"The payouts’ own currency (INR)."},"sourceTotal":{"$ref":"#/components/schemas/Decimal"},"rate":{"type":"string","pattern":"^\\d+(\\.\\d+)?$","description":"Units of the sent currency per unit of the payout’s currency, as a decimal string (ECB reference rates, crossed through EUR).","example":"0.011958904110"},"rateSource":{"type":"string","enum":["ecb"]},"rateDate":{"type":"string","description":"The ECB reference date of the rate (YYYY-MM-DD)."},"quotedAt":{"type":"string","format":"date-time","description":"When the quote this batch was sent with was made.","pattern":"Z$"}},"required":["sourceCurrency","sourceTotal","rate","rateSource","rateDate","quotedAt"]},{"type":"null"}],"description":"A batch sending balances PayPal can’t send in their own currency, at a quoted rate. Null otherwise."},"state":{"type":"string","enum":["intent","posted","submitted","completed","failed","denied","released","reconcile_required"],"description":"`intent`: committed, not sent yet. `posted`: sent, PayPal’s answer not in yet (re-sent automatically; PayPal never makes a second batch for the same id). `submitted`: PayPal has it and is paying. `completed`: every payout paid or released. `failed`: PayPal refused it outright; nothing was sent and the payouts are back to pending. `denied`: PayPal denied the batch; payouts released. `released`: you released it after checking PayPal. `reconcile_required`: OpenPartner can’t tell whether it went out; check PayPal, then `POST /payouts/paypal-batches/{batchId}/resolve`."},"payoutBatchId":{"type":["string","null"],"description":"PayPal’s id for the batch."},"batchStatus":{"type":["string","null"],"description":"PayPal’s batch_status: PENDING, PROCESSING, SUCCESS, DENIED, CANCELED."},"summary":{"type":"object","properties":{"paid":{"type":"integer"},"released":{"type":"integer","description":"Back to pending: failed, returned, refunded, or the batch didn’t go out."},"unclaimed":{"type":"integer","description":"PayPal emailed the partner to claim it (no PayPal account at that email). Returned after 30 days unless claimed."},"processing":{"type":"integer"}},"required":["paid","released","unclaimed","processing"]},"rehearsal":{"anyOf":[{"type":"object","properties":{"paid":{"type":"integer"},"unclaimed":{"type":"integer"},"failed":{"type":"integer"}},"required":["paid","unclaimed","failed"]},{"type":"null"}],"description":"A finished **sandbox** batch: what PayPal’s sandbox did with its test money. A sandbox batch never marks a payout paid; once PayPal settles every item its payouts go back to pending, ready for a live batch or a file."},"lastError":{"type":["string","null"]},"createdAt":{"type":"string","format":"date-time","pattern":"Z$"},"createdBy":{"type":"string","enum":["admin","api"]},"completedAt":{"type":["string","null"],"format":"date-time","pattern":"Z$"},"lastSyncedAt":{"type":["string","null"],"format":"date-time","pattern":"Z$"}},"required":["batchId","runId","environment","currency","itemCount","total","conversion","state","payoutBatchId","batchStatus","summary","rehearsal","lastError","createdAt","createdBy","completedAt","lastSyncedAt"]},"PaypalConversionQuote":{"type":"object","properties":{"quoteId":{"type":"string","description":"Send with it (`POST /payouts/runs/{runId}/paypal-batches` with `currency`, `sendAs` and `quoteId`) before `expiresAt`. Signed; it reserves nothing."},"runId":{"type":"string"},"environment":{"type":"string","enum":["live","sandbox"],"description":"The PayPal app the batch would send from: your live app, or a sandbox test. If another app is made active before you send, the send is refused (`409 quote_changed`)."},"currency":{"type":"string","description":"The payouts’ own currency (INR)."},"sendAs":{"type":"string","enum":["USD"]},"rate":{"type":"string","pattern":"^\\d+(\\.\\d+)?$","description":"Units of the sent currency per unit of the payout’s currency, as a decimal string (ECB reference rates, crossed through EUR).","example":"0.011958904110"},"rateSource":{"type":"string","enum":["ecb"]},"rateDate":{"type":"string","description":"The ECB reference date (YYYY-MM-DD). The ECB publishes once a business day, around 16:00 CET."},"quotedAt":{"type":"string","format":"date-time","pattern":"Z$"},"expiresAt":{"type":"string","format":"date-time","description":"15 minutes after `quotedAt`.","pattern":"Z$"},"count":{"type":"integer"},"sourceTotal":{"allOf":[{"$ref":"#/components/schemas/Decimal"},{"description":"What the quoted payouts add up to, in `currency`."}]},"sentTotal":{"allOf":[{"$ref":"#/components/schemas/Decimal"},{"description":"What PayPal would send, in `sendAs`: the sum of the per-item amounts, which can differ by a few cents from sourceTotal × rate."}]},"items":{"type":"array","items":{"type":"object","properties":{"payoutId":{"type":"string"},"partnerId":{"type":"string"},"partnerName":{"type":"string"},"destination":{"type":"string","description":"The PayPal account it goes to, masked. A change before sending voids the quote."},"amount":{"allOf":[{"$ref":"#/components/schemas/Decimal"},{"description":"The payout’s own amount, in `currency`."}]},"sentAmount":{"allOf":[{"$ref":"#/components/schemas/Decimal"},{"description":"What PayPal would send for it, in `sendAs`: amount × rate, rounded half up to the cent on its own."}]}},"required":["payoutId","partnerId","partnerName","destination","amount","sentAmount"]}},"leftOut":{"type":"array","items":{"type":"object","properties":{"payoutId":{"type":"string"},"partnerId":{"type":"string"},"partnerName":{"type":"string"},"destination":{"type":"string","description":"The PayPal account it goes to, masked. A change before sending voids the quote."},"amount":{"allOf":[{"$ref":"#/components/schemas/Decimal"},{"description":"The payout’s own amount, in `currency`."}]},"sentAmount":{"allOf":[{"$ref":"#/components/schemas/Decimal"},{"description":"What PayPal would send for it, in `sendAs`: amount × rate, rounded half up to the cent on its own."}]},"reason":{"type":"string","enum":["over_paypal_limit","below_minimum"]}},"required":["payoutId","partnerId","partnerName","destination","amount","sentAmount","reason"]},"description":"Payouts this batch can’t take at this rate: over PayPal’s per-payment limit in `sendAs` (USD 60,000), or under a cent in it. Pay them another way."}},"required":["quoteId","runId","environment","currency","sendAs","rate","rateSource","rateDate","quotedAt","expiresAt","count","sourceTotal","sentTotal","items","leftOut"]},"PaypalPayoutSettings":{"type":"object","properties":{"sendUnsupportedAs":{"type":["string","null"],"enum":["USD",null],"description":"Pay balances PayPal can’t send in their own currency (INR) through Pay with PayPal in this currency, at the ECB reference rate quoted before each batch. Null (the default): off, and those payouts stay `currency_not_supported`."}},"required":["sendUnsupportedAs"]},"PartnerPayoutProfileResponse":{"type":"object","properties":{"profile":{"anyOf":[{"$ref":"#/components/schemas/PayoutProfile"},{"type":"null"}],"description":"Null until the partner (or you) gives payout details."},"paidVia":{"type":"string","enum":["manual","stripe_connect"],"description":"How this partner's payouts go out under your payout rail today. Payout details are only used for `manual`."},"rail":{"type":"string","enum":["auto","stripe_connect","manual"],"description":"The brand's payout rail setting. `manual`: the brand pays every partner itself (bank transfer, PayPal or Wise batch files) from these details, and Stripe Connect isn't used. `stripe_connect`: every partner is paid through Stripe Connect. `auto`: Stripe Connect when the partner has connected an account, otherwise manual."}},"required":["profile","paidVia","rail"]},"PayoutProfile":{"type":"object","properties":{"method":{"type":"string","enum":["paypal","bank","upi","other"]},"country":{"type":["string","null"],"description":"Where the account is held (ISO 3166-1 alpha-2); `IN` for UPI; null for a PayPal email or instructions without one."},"details":{"anyOf":[{"$ref":"#/components/schemas/PayoutDetails"},{"type":"null"}],"description":"Null when the stored details can't be read on this deployment (restored from an encrypted copy elsewhere): the partner has to enter them again."},"summary":{"type":["string","null"],"description":"A masked one-line description, safe to show anywhere, e.g. `Bank IBAN GB •••• 5432 (GBP)`."},"updatedAt":{"type":"string","format":"date-time","pattern":"Z$"},"updatedBy":{"type":"string","enum":["partner","admin","api"],"description":"Who saved them last: the partner (portal), an admin (portal) or an API key."}},"required":["method","country","details","summary","updatedAt","updatedBy"]},"PayoutDetails":{"oneOf":[{"type":"object","properties":{"method":{"type":"string","enum":["paypal"]},"email":{"type":"string","maxLength":254,"format":"email"},"country":{"type":"string"}},"required":["method","email"]},{"type":"object","properties":{"method":{"type":"string","enum":["bank"]},"accountHolderName":{"type":"string","minLength":1,"maxLength":140},"accountHolderType":{"type":"string","enum":["person","business"],"default":"person"},"country":{"type":"string"},"currency":{"type":"string","pattern":"^[A-Za-z]{3}$"},"iban":{"type":"string"},"bic":{"type":"string"},"accountNumber":{"type":"string"},"sortCode":{"type":"string"},"routingNumber":{"type":"string"},"accountType":{"type":"string","enum":["checking","savings"]},"ifsc":{"type":"string"},"email":{"type":"string","maxLength":254,"format":"email"},"address":{"type":"object","properties":{"line1":{"type":"string","minLength":1,"maxLength":140},"city":{"type":"string","minLength":1,"maxLength":80},"postCode":{"type":"string","maxLength":20},"state":{"type":"string","maxLength":40},"country":{"type":"string"}},"required":["line1","city","country"]}},"required":["method","accountHolderName","country","currency"]},{"type":"object","properties":{"method":{"type":"string","enum":["upi"]},"vpa":{"type":"string","pattern":"^[A-Za-z0-9._-]{2,256}@[A-Za-z][A-Za-z0-9]{1,63}$"},"name":{"type":"string","maxLength":140}},"required":["method","vpa"]},{"type":"object","properties":{"method":{"type":"string","enum":["other"]},"instructions":{"type":"string","minLength":1,"maxLength":1000},"country":{"type":"string"}},"required":["method","instructions"]}],"description":"Where the partner wants manual payouts sent, by `method`: `paypal` (the PayPal account email), `bank` (an IBAN, or a local account number with what routes to it: `sortCode` for GBP, `routingNumber` + `accountType` + `address` for USD, `ifsc` for INR, `bic` + `address` for any other currency; an `address` for any USD, PHP, THB or TRY account, with its `state` in the US, Canada or Brazil), `upi` (an Indian UPI ID), or `other` (free-text instructions). Values are validated (IBAN checksum, ABA checksum, IFSC shape) and normalised (upper case, spaces removed)."},"PartnerDashboard":{"type":"object","properties":{"partnerId":{"type":"string"},"since":{"type":"string","format":"date-time","pattern":"Z$"},"currency":{"type":"string","description":"The brand's default currency: the currency of `attributedRevenue` and `commissionByStatus`.","example":"USD"},"clicks":{"type":"integer"},"attributedEvents":{"type":"integer"},"attributedRevenue":{"type":"number","description":"Attributed revenue in `currency` only."},"attributedRevenueByCurrency":{"type":"object","additionalProperties":{"type":"number"},"description":"Amount per ISO 4217 currency code. Currencies with nothing are absent; nothing is converted."},"commissionByStatus":{"type":"object","properties":{"accrued":{"type":"number"},"approved":{"type":"number"},"paid":{"type":"number"},"reversed":{"type":"number"}},"description":"Sum of commission amounts in `currency` accrued since `since`, keyed by status. Statuses with nothing are absent. Commissions in other currencies are in `commissionByCurrency`. `paid` includes manual payouts not yet sent: read payouts for money that went out."},"commissionByCurrency":{"type":"object","additionalProperties":{"type":"object","properties":{"accrued":{"type":"number"},"approved":{"type":"number"},"paid":{"type":"number"},"reversed":{"type":"number"}}},"description":"Commission totals per currency, then per status. Currencies and statuses with nothing are absent."},"links":{"type":"array","items":{"type":"object","properties":{"linkKey":{"type":"string"},"clicks":{"type":"integer"},"attributedEvents":{"type":"integer"},"attributedRevenue":{"type":"number","description":"In `currency` only."},"attributedRevenueByCurrency":{"type":"object","additionalProperties":{"type":"number"},"description":"Amount per ISO 4217 currency code. Currencies with nothing are absent; nothing is converted."}},"required":["linkKey","clicks","attributedEvents","attributedRevenue","attributedRevenueByCurrency"]},"description":"Only with `includeLinks=true`."}},"required":["partnerId","since","currency","clicks","attributedEvents","attributedRevenue","attributedRevenueByCurrency","commissionByStatus","commissionByCurrency"]},"PartnerTimeseries":{"type":"object","properties":{"partnerId":{"type":"string"},"since":{"type":"string","format":"date-time","pattern":"Z$"},"until":{"type":"string","format":"date-time","pattern":"Z$"},"bucket":{"type":"string","enum":["day","week"]},"buckets":{"type":"array","items":{"type":"object","properties":{"bucket":{"type":"string","format":"date-time","pattern":"Z$"},"clicks":{"type":"integer"},"leads":{"type":"integer","description":"`signup` and `trial_started` events."},"sales":{"type":"integer","description":"Events with a value above 0."},"revenue":{"type":"number"},"earnings":{"type":"number","description":"Commission accrued in the bucket."}},"required":["bucket","clicks","leads","sales","revenue","earnings"]}}},"required":["partnerId","since","until","bucket","buckets"]},"PartnerEventList":{"type":"object","properties":{"partnerId":{"type":"string"},"events":{"type":"array","items":{"type":"object","properties":{"attributionId":{"type":"string"},"weight":{"type":"number"},"model":{"type":"string","enum":["last_click","first_click","linear","position"]},"eventId":{"type":"string"},"type":{"type":"string"},"value":{"type":["number","null"]},"currency":{"type":["string","null"]},"ts":{"type":"string","format":"date-time","pattern":"Z$"},"userId":{"type":["string","null"],"description":"The brand's userId. Null when a partner calls: partners get `referralId` instead."},"referralId":{"type":"string","description":"The customer's pseudonymous id, `ref_` + 12 characters: the same customer has the same id on `/partners/{id}/referrals`, `/events` and `/customers`, and a partner can't work out the userId from it.","example":"ref_Xk3dP0aQm9Lc"},"click":{"type":["object","null"],"properties":{"id":{"type":"string"},"linkKey":{"type":["string","null"]},"referer":{"type":["string","null"]},"landingUrl":{"type":["string","null"]},"ts":{"type":["string","null"],"format":"date-time","pattern":"Z$"},"subId":{"type":["string","null"],"description":"The `sub_id` on the partner link the click came through, or null."}},"required":["id","linkKey","referer","landingUrl","ts","subId"]}},"required":["attributionId","weight","model","eventId","type","value","currency","ts","userId","referralId","click"]}},"limit":{"type":"integer"},"offset":{"type":"integer"}},"required":["partnerId","events","limit","offset"]},"PartnerCustomerList":{"type":"object","properties":{"partnerId":{"type":"string"},"customers":{"type":"array","items":{"type":"object","properties":{"userId":{"type":["string","null"],"description":"The brand's userId. Null when a partner calls: partners get `referralId` instead."},"referralId":{"type":"string","description":"The customer's pseudonymous id, `ref_` + 12 characters: the same customer has the same id on `/partners/{id}/referrals`, `/events` and `/customers`, and a partner can't work out the userId from it.","example":"ref_Xk3dP0aQm9Lc"},"firstEventAt":{"type":"string","format":"date-time","pattern":"Z$"},"lastEventAt":{"type":"string","format":"date-time","pattern":"Z$"},"eventCount":{"type":"integer"},"revenue":{"type":"number"}},"required":["userId","referralId","firstEventAt","lastEventAt","eventCount","revenue"]}},"limit":{"type":"integer"},"offset":{"type":"integer"}},"required":["partnerId","customers","limit","offset"]},"PartnerBreakdown":{"type":"object","properties":{"dimension":{"type":"string"},"rows":{"type":"array","items":{"type":"object","properties":{"value":{"type":"string"},"count":{"type":"integer"}},"required":["value","count"]}},"sampleSize":{"type":"integer","description":"User-agent dimensions only: clicks sampled (newest first)."},"sampleCapped":{"type":"boolean"}},"required":["dimension","rows"]},"PartnerFunnel":{"type":"object","properties":{"partnerId":{"type":"string"},"since":{"type":"string","format":"date-time","pattern":"Z$"},"stages":{"type":"object","properties":{"clicks":{"type":"integer","description":"The partner's clicks since `since`, excluding fraud-flagged ones. Every later stage follows these clicks."},"identities":{"type":"integer","description":"Distinct users stitched to those clicks."},"signups":{"type":"integer","description":"Of those users, the ones with a `signup`, `trial_started`, `subscription_created` or `invoice_paid` event attributed to this partner through one of those clicks."},"paid":{"type":"integer","description":"Of those, the ones with an attributed `invoice_paid` event."}},"required":["clicks","identities","signups","paid"]},"rates":{"type":"object","properties":{"stitchRate":{"type":"number"},"signupRate":{"type":"number"},"paidRate":{"type":"number"},"overall":{"type":"number"}},"required":["stitchRate","signupRate","paidRate","overall"]},"currency":{"type":"string","description":"The brand's default currency: the currency of `revenue`.","example":"USD"},"revenue":{"type":"number","description":"What the paying users in `paid` have paid so far (attributed `invoice_paid`, the partner's weighted share), in `currency` only."},"revenueByCurrency":{"type":"object","additionalProperties":{"type":"number"},"description":"Amount per ISO 4217 currency code. Currencies with nothing are absent; nothing is converted."}},"required":["partnerId","since","stages","rates","currency","revenue","revenueByCurrency"]},"PartnerReferralPage":{"type":"object","properties":{"partnerId":{"type":"string"},"stages":{"type":"array","items":{"$ref":"#/components/schemas/ReferralStageDefinition"},"description":"The stages the brand shows partners, in its order (`GET /config/referrals`). Render each referral's progress against this list. Every other event type is hidden."},"currency":{"type":"string","description":"The brand's default currency: the currency of each `commission`.","example":"INR"},"referrals":{"type":"array","items":{"type":"object","properties":{"referralId":{"type":"string","description":"The customer's pseudonymous id, `ref_` + 12 characters: the same customer has the same id on `/partners/{id}/referrals`, `/events` and `/customers`, and a partner can't work out the userId from it.","example":"ref_Xk3dP0aQm9Lc"},"number":{"type":"integer","description":"1 for the partner's first referral, counted in the order customers became the partner's referrals, so a new one takes the next number. Only an admin re-running attribution (unflagging a click) can renumber; `referralId` never changes."},"label":{"type":"string","description":"What to call the customer: `customerLabel` when there is one, else \"Referral #\" and `number`.","example":"Referral #12"},"customerLabel":{"type":["string","null"],"description":"The newest `metadata.customerLabel` the brand sent on this customer's credited events, cut to 80 characters. Null unless the brand shows customer labels (`showCustomerLabels`, off by default)."},"userId":{"type":"string","description":"The brand's userId. Admins and scoped keys only; never sent to a partner."},"referredAt":{"type":"string","format":"date-time","description":"When the customer first clicked the partner's link: the earliest of the partner's clicks credited on the customer's events.","pattern":"Z$"},"stages":{"type":"array","items":{"type":"object","properties":{"eventType":{"type":"string"},"label":{"type":"string"},"firstAt":{"type":"string","format":"date-time","description":"The first event of this type: when the customer reached the stage.","pattern":"Z$"},"lastAt":{"type":"string","format":"date-time","description":"The latest event of this type (a monthly invoice repeats).","pattern":"Z$"},"count":{"type":"integer"},"reversed":{"type":"boolean","description":"Every event at this stage earned the partner a commission and none of it is left: reversed (a refund, a chargeback, or the brand reversing it) or clawed back after payment."}},"required":["eventType","label","firstAt","lastAt","count","reversed"]},"description":"The shown stages the customer has reached, in the brand's order. Stages not reached are absent."},"latestStage":{"type":["object","null"],"properties":{"eventType":{"type":"string"},"label":{"type":"string"},"at":{"type":"string","format":"date-time","pattern":"Z$"}},"required":["eventType","label","at"],"description":"The furthest stage reached in the brand's order, skipping reversed ones. `at` is when it was reached. Null when none is."},"commission":{"type":"number","description":"What the customer has earned the partner in `currency`, every event type included: live commissions (not reversed), net of refund clawbacks. Bonuses aren't tied to a customer and never count."},"commissionByCurrency":{"type":"object","additionalProperties":{"type":"number"},"description":"Amount per ISO 4217 currency code. Currencies with nothing are absent; nothing is converted."}},"required":["referralId","number","label","customerLabel","referredAt","stages","latestStage","commission","commissionByCurrency"]}},"total":{"type":"integer","description":"All of the partner's referrals, not only this page."},"nextCursor":{"type":["string","null"]}},"required":["partnerId","stages","currency","referrals","total","nextCursor"]},"ReferralStageDefinition":{"type":"object","properties":{"eventType":{"type":"string","description":"The event type, as sent to `POST /attribution/events`.","example":"demo_booked"},"label":{"type":"string","description":"What partners see.","example":"Demo booked"}},"required":["eventType","label"]},"WebhookEndpoint":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"url":{"type":"string","format":"uri"},"secretPrefix":{"type":"string","description":"First characters of the signing secret, to tell secrets apart. The secret itself is stored encrypted and only returned at creation and rotation."},"events":{"type":"array","items":{"type":"string"}},"active":{"type":"boolean"},"label":{"type":["string","null"]},"createdAt":{"type":"string","format":"date-time","pattern":"Z$"},"lastUsedAt":{"type":["string","null"],"format":"date-time","pattern":"Z$"}},"required":["id","tenantId","url","secretPrefix","events","active","label","createdAt","lastUsedAt"]},"CreateWebhookRequest":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"events":{"type":"array","items":{"type":"string","enum":["attribution.created","commission.accrued","commission.approved","commission.paid","commission.reversed","bonus.awarded","bonus.approved","bonus.paid","bonus.reversed","partner.created","partner.activated","partnership.approved","payout.created","*"]},"minItems":1},"label":{"type":"string","maxLength":80}},"required":["url","events"]},"UpdateWebhookRequest":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"events":{"type":"array","items":{"type":"string","enum":["attribution.created","commission.accrued","commission.approved","commission.paid","commission.reversed","bonus.awarded","bonus.approved","bonus.paid","bonus.reversed","partner.created","partner.activated","partnership.approved","payout.created","*"]},"minItems":1},"label":{"type":["string","null"],"maxLength":80},"active":{"type":"boolean"}}},"WebhookDelivery":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"endpointId":{"type":"string"},"eventId":{"type":"string","description":"The envelope `id` (`evt_…`), also sent as `X-OpenPartner-Delivery`."},"eventType":{"type":"string"},"payload":{"description":"The exact envelope that was sent."},"status":{"type":"string","enum":["pending","delivered","failed"]},"httpStatus":{"type":["integer","null"]},"error":{"type":["string","null"]},"attempts":{"type":"integer"},"createdAt":{"type":"string","format":"date-time","pattern":"Z$"},"deliveredAt":{"type":["string","null"],"format":"date-time","pattern":"Z$"},"lastAttemptAt":{"type":["string","null"],"format":"date-time","pattern":"Z$"}},"required":["id","tenantId","endpointId","eventId","eventType","status","httpStatus","error","attempts","createdAt","deliveredAt","lastAttemptAt"]},"CreatedKey":{"type":"object","properties":{"id":{"type":"string"},"plaintext":{"type":"string","description":"The key. Shown once and never stored; keep it server-side."}},"required":["id","plaintext"]},"CreateKeyRequest":{"type":"object","properties":{"label":{"type":"string"}}},"CreateScopedKeyRequest":{"type":"object","properties":{"scopes":{"type":"array","items":{"type":"string","minLength":3,"maxLength":64},"minItems":1},"label":{"type":"string"}},"required":["scopes"]},"ApiKeySummary":{"type":"object","properties":{"id":{"type":"string"},"prefix":{"type":"string"},"label":{"type":["string","null"]},"scopes":{"type":"array","items":{"type":"string"}},"createdAt":{"type":"string","format":"date-time","pattern":"Z$"},"lastUsedAt":{"type":["string","null"],"format":"date-time","pattern":"Z$"},"revokedAt":{"type":["string","null"],"format":"date-time","pattern":"Z$"}},"required":["id","prefix","label","createdAt","lastUsedAt","revokedAt"]},"ExportBundle":{"type":"object","properties":{"exportedAt":{"type":"string","format":"date-time","pattern":"Z$"},"schemaVersion":{"type":"integer"},"tables":{"type":"object","additionalProperties":{"type":"array","items":{"type":"object","additionalProperties":{}}}}},"required":["exportedAt","schemaVersion","tables"],"description":"The whole tenant, re-importable by a self-hosted instance (`POST /import`)."},"ImportResult":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"report":{"type":"object","properties":{"inserted":{"type":"object","additionalProperties":{"type":"integer"}},"skipped":{"type":"object","additionalProperties":{"type":"integer"}},"credentials":{"type":"object","additionalProperties":{"type":"array","items":{"type":"object","additionalProperties":{}}},"description":"Fresh credentials for restored rows, per table, shown once. `WebhookEndpoint`: `{ id, url, secret }` for each endpoint restored (paused until you turn it back on)."}},"required":["inserted","skipped","credentials"]}},"required":["ok","report"]},"ImportRequest":{"type":"object","properties":{"schemaVersion":{"type":"integer"},"tables":{"type":"object","additionalProperties":{"type":"array","items":{"type":"object","additionalProperties":{}}}}},"required":["schemaVersion","tables"]},"NetworkMembership":{"type":"object","properties":{"enabled":{"type":"boolean"},"networkUrl":{"type":"string"},"hasVendorToken":{"type":"boolean"},"scopedKeyId":{"type":["string","null"]},"autoEnroll":{"type":"boolean","description":"Stored name for \"offer the Network to my partners\". When `enabled && autoEnroll`, partners are asked whether to join, and only those who opt in are shared (name and email; never `metadata`)."},"bonusesAuthorized":{"type":"boolean","description":"The Network's federation key holds `bonuses:write`, so OpenPartner Network bounties can pay your partners through `POST /bonuses`. Keys from before Network bounties don't: `POST /config/network/authorize-bonuses` adds it."}},"required":["enabled","networkUrl","hasVendorToken","scopedKeyId","autoEnroll","bonusesAuthorized"]},"UpdateNetworkMembershipRequest":{"type":"object","properties":{"enabled":{"type":"boolean"},"networkUrl":{"anyOf":[{"type":"string","format":"uri"},{"type":"string","enum":[""]}]},"vendorToken":{"type":"string","maxLength":500},"scopedKeyId":{"type":["string","null"]},"autoEnroll":{"type":"boolean"}}},"PartnerAccessSettings":{"type":"object","properties":{"uninvitedPortalAccess":{"type":"boolean","description":"Portal access for partners added without an invite: `POST /partners` with `sendInvite: false` and no `portalAccess` (except partners the OpenPartner Network provisions), and every `POST /import/partners-csv` row. Default `true`. Turn it off before creating a headless program's partners."}},"required":["uninvitedPortalAccess"]},"UpdatePartnerAccessSettingsRequest":{"type":"object","properties":{"uninvitedPortalAccess":{"type":"boolean"}}},"PayoutStatementSettings":{"type":"object","properties":{"legalName":{"type":["string","null"],"description":"The registered business name statements are issued in. Null: the program name."},"address":{"type":["string","null"],"description":"Your postal address; line breaks are kept."},"footer":{"type":["string","null"],"description":"Printed at the foot of every statement; line breaks are kept."},"numberPrefix":{"type":["string","null"],"description":"Put before the payout id to make the statement number, e.g. `PS-`."},"taxLines":{"type":"object","properties":{"enabled":{"type":"boolean"},"labels":{"type":"array","items":{"type":"string"}}},"required":["enabled","labels"],"description":"A placeholder for tax lines such as tax withheld, off by default. Turned on, each label prints as a row marked \"Not recorded\": OpenPartner records no tax amounts yet. Which lines you need (GST, TDS) is for your accountant to say."}},"required":["legalName","address","footer","numberPrefix","taxLines"]},"UpdatePayoutStatementSettingsRequest":{"type":"object","properties":{"legalName":{"type":["string","null"],"maxLength":200},"address":{"type":["string","null"],"maxLength":500},"footer":{"type":["string","null"],"maxLength":2000},"numberPrefix":{"type":["string","null"],"maxLength":20,"pattern":"^[A-Za-z0-9/_.#-]*$"},"taxLines":{"type":"object","properties":{"enabled":{"type":"boolean"},"labels":{"type":"array","items":{"type":"string","minLength":1,"maxLength":60},"minItems":1,"maxItems":5}}}}},"ReferralSettings":{"type":"object","properties":{"stages":{"type":"array","items":{"$ref":"#/components/schemas/ReferralStageDefinition"},"description":"The event types partners see as stages on their referrals, in order. Default: `signup`, `trial_started`, `subscription_created`, `invoice_paid`. A type that isn't listed is hidden from partners (it still makes the customer a referral and still counts toward the commission total). An empty list shows no stages."},"showCustomerLabels":{"type":"boolean","description":"Show partners the `metadata.customerLabel` you send on events. Off by default: OpenPartner stores only your userId, and a label is personal data you choose to share with your partners, as its controller."},"defaultStages":{"type":"array","items":{"$ref":"#/components/schemas/ReferralStageDefinition"},"description":"The stages a brand gets until it sets its own."},"recentEventTypes":{"type":"array","items":{"type":"string"},"description":"Event types recorded in the last 90 days, most frequent first (up to 50): candidates for stages."}},"required":["stages","showCustomerLabels","defaultStages","recentEventTypes"]},"UpdateReferralSettingsRequest":{"type":"object","properties":{"stages":{"type":"array","items":{"type":"object","properties":{"eventType":{"type":"string","minLength":1,"maxLength":100},"label":{"type":"string","minLength":1,"maxLength":60}},"required":["eventType","label"]},"maxItems":20},"showCustomerLabels":{"type":"boolean"}}}},"parameters":{}},"paths":{"/partners":{"post":{"operationId":"createPartner","tags":["Partners"],"summary":"Create a partner","description":"Creates a partner and grants it programs.\n\n- `sendInvite: false` creates the partner **active immediately** and sends no email. Use this when your own app is the partner's UI (headless / in-app referral). The default sends a magic-link invite.\n- `portalAccess: false` keeps the partner out of the hosted partner portal: `POST /auth/signin` sends them nothing (it answers as it does for an unknown email), and no invite or portal email goes to them. It needs `sendInvite: false` (`400 invite_requires_portal_access` otherwise). Omitted, it is `true` for an invited partner and the tenant default (`GET /config/partner-access`) for one created without an invite. Change it later with `PATCH /partners/{id}`.\n- `programIds` omitted grants **every current program**. Pass an explicit list (even `[]`) to control access.\n- A coupon is auto-minted per granted program.\n- Fires `partner.created`, then, for a `sendInvite: false` partner (active now), `partner.activated` with `invited: false`. An invited partner fires `partner.activated` when they accept.\n- `recruitedByPartnerId` names the partner who recruited this one (two-tier commissions). In programs with `recruiterCommissionPercent` that partner earns that share of this one's commissions from now on. The recruiter must be in this tenant and not revoked (`400 recruiter_not_found`, `400 recruiter_revoked`).\n\n**`partners:create`** is the scope for an in-app integration's runtime key: it can only create partners, and only without an invite or commission terms. A request with `sendInvite` omitted or `true`, a `commissionSnapshot`, `programCommissions` or `recruitedByPartnerId` answers `403 forbidden_scope` with `required: \"partners:write\"`.\n\n**`country`** (optional, ISO 3166-1 alpha-2) is where the partner is based. OpenPartner Cloud doesn't serve some countries: a blocked one answers `422 country_not_supported`, and `\"UA\"` needs `outsideOccupiedRegions: true` (`422 occupied_region_confirmation_required` otherwise). An accepted country is recorded, and if it is blocked later, the partner's Stripe payouts are held. Self-hosted installs ignore it.\n\n**OpenPartner Network:** this call shares nothing with the OpenPartner Network. A partner's name and email reach it only if the partner opts in themselves, while the tenant offers the Network (`autoEnroll` on, `GET /config/network`): they're asked on the join page, once on first portal sign-in, and in a portal setting. `metadata` is never sent. To keep customer-referral partners from being asked, turn offering off: `POST /config/network {\"autoEnroll\": false}`.\n\nOn OpenPartner Cloud the brand needs an active plan (`402 plan_required`) and an approved account (`403 brand_pending_review`).\n\n**Access:** admin key, or a scoped key with `partners:write` or `partners:create`.","security":[{"adminKey":[]},{"scopedKey":["partners:write"]},{"scopedKey":["partners:create"]}],"x-openpartner-access":{"kind":"admin","scopes":["partners:write","partners:create"],"partner":false},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePartnerRequest"}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerCreated"}}}},"400":{"description":"`invalid_body`, `tenant_slug_missing`, `unknown_campaign_ids`, `invite_requires_portal_access`, `recruiter_not_found`, `recruiter_revoked`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"`plan_required`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`, `brand_pending_review`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`email_taken`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"`country_not_supported`, `occupied_region_confirmation_required`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"operationId":"listPartners","tags":["Partners"],"summary":"List or search partners","description":"Newest first, keyset-paginated. `email` is an exact match (\"find by email\"); `q` is a case-insensitive substring match on name or email. Filters keep the same order and cursor scheme.\n\n**Access:** admin key, or a scoped key with `partners:read`.","security":[{"adminKey":[]},{"scopedKey":["partners:read"]}],"x-openpartner-access":{"kind":"admin","scopes":["partners:read"],"partner":false},"parameters":[{"schema":{"type":"string","format":"email"},"required":false,"name":"email","in":"query"},{"schema":{"type":"string","maxLength":200},"required":false,"name":"q","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200,"default":100},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string"},"required":false,"name":"cursor","in":"query"}],"responses":{"200":{"description":"One page.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerPage"}}}},"400":{"description":"`invalid_query`, `tenant_slug_missing`, `invalid_cursor`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/partners/{id}":{"get":{"operationId":"getPartner","tags":["Partners"],"summary":"Get a partner","description":"**Access:** admin key, a scoped key with `partners:read`, or the partner's own key (`:id` must be that partner).","security":[{"adminKey":[]},{"partnerKey":[]},{"scopedKey":["partners:read"]}],"x-openpartner-access":{"kind":"partnerOrAdmin","scopes":["partners:read"],"partner":true},"parameters":[{"schema":{"type":"string","description":"The id."},"required":true,"description":"The id.","name":"id","in":"path"}],"responses":{"200":{"description":"The partner.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Partner"}}}},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"operationId":"updatePartner","tags":["Partners"],"summary":"Change a partner's portal access or recruiter","description":"`portalAccess: false` ends the partner's portal sessions and unused sign-in links at once. From then on `POST /auth/signin` sends them nothing, and no invite or portal email goes to them. Their partner API keys (`POST /partners/{id}/api-keys`) keep working; revoke those with `DELETE /api-keys/{keyId}`.\n\n`portalAccess: true` sends nothing. An active partner can then request a sign-in link on the portal; one who never accepted an invite needs `POST /partners/{id}/invite`.\n\n`recruitedByPartnerId` sets, changes or (`null`) clears who recruited this partner, for two-tier commissions. The recruitment starts at the change (`recruitedAt`), so a new recruiter earns only on commissions from then on, and commissions already accrued stay with whoever earned them. Refused: the partner themselves (`recruiter_is_partner`), a partner this one recruited (`recruiter_cycle`: two partners can't recruit each other), a revoked partner or one not in this tenant.\n\nFields this operation doesn't accept answer `400 invalid_body`.\n\n**Access:** admin key, or a scoped key with `partners:write`.","security":[{"adminKey":[]},{"scopedKey":["partners:write"]}],"x-openpartner-access":{"kind":"admin","scopes":["partners:write"],"partner":false},"parameters":[{"schema":{"type":"string","description":"The id."},"required":true,"description":"The id.","name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePartnerRequest"}}}},"responses":{"200":{"description":"The partner, as saved.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Partner"}}}},"400":{"description":"`invalid_body`, `tenant_slug_missing`, `recruiter_not_found`, `recruiter_is_partner`, `recruiter_revoked`, `recruiter_cycle`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/partners/{id}/recruits":{"get":{"operationId":"getPartnerRecruits","tags":["Partners"],"summary":"A partner's recruit link and recruits","description":"Two-tier commissions: in a program with `recruiterCommissionPercent`, a partner earns that share of each commission earned by the partners they recruited. One level only: they earn nothing on partners their recruits bring in.\n\n- **`recruitUrl`** is the join page with the partner's `ref`. Someone who joins through it is recorded as their recruit (`recruitedByPartnerId`). The code is minted on the first read while a program pays a share; until then, and for a revoked partner, the link is null.\n- **`programs`**: the programs that pay a share. A partner calling sees the running programs they're granted; an admin or a `partners:read` key sees them all.\n- **`recruits`**: newest first (up to `limit`), with what each has earned the partner per currency: live recruiter commissions net of clawbacks. `total` counts all of them.\n\nA recruiter commission is a commission like any other, on the recruit's sale (`sourceCommissionId` names the recruit's commission). It is approved together with that commission, cut by the same share when the sale is refunded, and reversed when an admin reverses it.\n\n**Access:** admin key, a scoped key with `partners:read`, or the partner's own key (`:id` must be that partner).","security":[{"adminKey":[]},{"partnerKey":[]},{"scopedKey":["partners:read"]}],"x-openpartner-access":{"kind":"partnerOrAdmin","scopes":["partners:read"],"partner":true},"parameters":[{"schema":{"type":"string","description":"The id."},"required":true,"description":"The id.","name":"id","in":"path"}],"responses":{"200":{"description":"The partner's recruiting.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerRecruits"}}}},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/import/partners-csv":{"post":{"operationId":"importPartnersCsv","tags":["Partners"],"summary":"Bulk-create partners from CSV","description":"Creates many partners in one request: the way to load an existing customer base without spending your rate limit on one `POST /partners` per row.\n\nSend `text/csv` with a header row: `email` and `name` (required), `activatedAt` (ISO-8601, default now) and `metadata` (a JSON object string, e.g. `\"{\"\"appUserId\"\":\"\"u_1\"\"}\"`). Quoted fields may contain commas but not line breaks. Tools that can't send `text/csv` can POST `{\"csv\": \"...\"}` as JSON instead.\n\nUnlike `POST /partners`, an import:\n- grants every partner **every current program** (there is no `programIds`),\n- sends no email, mints no coupons, fires no `partner.created` or `partner.activated` webhooks,\n- gives every partner the tenant's default portal access for partners added without an invite (`GET /config/partner-access`),\n- shares nothing with the OpenPartner Network: as with `POST /partners`, only a partner's own opt-in does.\n\nRows whose email already exists are reported as `skipped_email_taken` with the existing `partnerId`, so re-running an import is safe. `?dryRun=true` validates and reports without writing. The file is written in one transaction, so a failed import writes nothing. A repeated email later in the same file is skipped like one already on the roster. Send very large rosters in batches of a few thousand rows.\n\n**Access:** admin key, or a scoped key with `partners:write`.","security":[{"adminKey":[]},{"scopedKey":["partners:write"]}],"x-openpartner-access":{"kind":"admin","scopes":["partners:write"],"partner":false},"parameters":[{"schema":{"type":"string","enum":["0","1","true","false"],"description":"Validate and report without writing. Not validated by the route: only `true` and `1` switch it on."},"required":false,"description":"Validate and report without writing. Not validated by the route: only `true` and `1` switch it on.","name":"dryRun","in":"query"}],"requestBody":{"required":true,"content":{"text/csv":{"schema":{"type":"string","example":"email,name,metadata\nada@example.com,Ada Lovelace,\"{\"\"appUserId\"\":\"\"u_1\"\"}\"\n"}},"application/json":{"schema":{"type":"object","properties":{"csv":{"type":"string"}},"required":["csv"]}}}},"responses":{"200":{"description":"Per-row results.","content":{"application/json":{"schema":{"type":"object","properties":{"dryRun":{"type":"boolean"},"summary":{"type":"object","properties":{"total":{"type":"integer"},"created":{"type":"integer"},"skipped":{"type":"integer"},"invalid":{"type":"integer"}},"required":["total","created","skipped","invalid"]},"report":{"type":"array","items":{"type":"object","properties":{"row":{"type":"integer","description":"Line number in the file, counting the header as 1."},"email":{"type":"string"},"status":{"type":"string","enum":["created","skipped_email_taken","invalid"]},"reason":{"type":"string","enum":["invalid_email","missing_name","metadata_not_valid_json","invalid_activatedAt"]},"partnerId":{"type":"string"}},"required":["row","email","status"]}}},"required":["dryRun","summary","report"]}}}},"400":{"description":"`invalid_query`, `tenant_slug_missing`, `empty_body`, `invalid_csv`, `no_rows`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"`trial_expired`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`, `brand_pending_review`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/partners/{id}/invite":{"post":{"operationId":"invitePartner","tags":["Partners"],"summary":"Send or resend the invite email","description":"For a partner who has not activated yet. For an applicant held for review, this is also the approval. A partner without portal access gets `409 portal_access_disabled` (the invite is a sign-in link), and an applicant stays unapproved.\n\n**Access:** admin key, or a scoped key with `partners:write`.","security":[{"adminKey":[]},{"scopedKey":["partners:write"]}],"x-openpartner-access":{"kind":"admin","scopes":["partners:write"],"partner":false},"parameters":[{"schema":{"type":"string","description":"The id."},"required":true,"description":"The id.","name":"id","in":"path"}],"responses":{"200":{"description":"Sent.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"approved":{"type":"boolean"}},"required":["ok","approved"]}}}},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`already_activated`, `portal_access_disabled`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"`mail_send_failed`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/partners/{id}/signin-link":{"post":{"operationId":"createPartnerSigninLink","tags":["Partners"],"summary":"Sign a partner in to the portal from your app","description":"Mints a link that signs the partner in to the partner portal, so your users go from your app to their referral dashboard without a second login. Call it from your backend when the user clicks through (a \"Refer & earn\" button that hits your server), and redirect the browser to `url` at once: it works **once**, for **five minutes**, and signs in whoever opens it. Don't email it, log it, or render it into a page ahead of time.\n\nThe link goes to your white-label domain when one is verified (`refer.example.com/auth/magic?…`), otherwise to the portal on OpenPartner's own domain. `returnTo` (a portal path such as `/links`) picks the page it lands on.\n\nThe partner must be one `POST /auth/signin` would email: active, not revoked, with portal access. Otherwise `409`: `partner_revoked`, `pending_review` (an applicant not yet approved), `portal_access_disabled` (turn it on with `PATCH /partners/{id}`), or `partner_not_activated` (invited, but hasn't accepted; partners created with `sendInvite: false` are active at once). The checks run again when the link is opened, along with the served-country check on the partner's own connection.\n\n**`partners:signin`** is the scope for this, and nothing else grants it, `partners:write` included. The OpenPartner Network's federation key is refused even if it holds the scope. Portal sessions get `403 api_key_required`: this is a server-to-server call. Each key may mint 600 links a minute, and each partner 10 (`429 rate_limited`), and every link minted is recorded with the key that asked for it.\n\nOn OpenPartner Cloud the brand needs an approved account (`403 brand_pending_review`).\n\n**Access:** admin key, or a scoped key with `partners:signin`.","security":[{"adminKey":[]},{"scopedKey":["partners:signin"]}],"x-openpartner-access":{"kind":"admin","scopes":["partners:signin"],"partner":false},"parameters":[{"schema":{"type":"string","description":"The id."},"required":true,"description":"The id.","name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerSigninLinkRequest"}}}},"responses":{"201":{"description":"Minted. Redirect to `url` now.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerSigninLink"}}}},"400":{"description":"`invalid_body`, `tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`, `api_key_required`, `brand_pending_review`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`partner_not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`partner_revoked`, `pending_review`, `portal_access_disabled`, `partner_not_activated`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/partners/signin-link":{"post":{"operationId":"lookupPartnerSigninLink","tags":["Partners"],"summary":"Sign a partner in, by email or your own user id","description":"The same as `POST /partners/{id}/signin-link`, naming the partner by something your app already holds instead of the OpenPartner id: exactly one of `email` or `metadata`.\n\n- `email` is unique per brand. Send only addresses your app has verified.\n- `metadata` is one key and value you stored on the partner when you created it, e.g. `{ \"appUserId\": \"u_123\" }` for `POST /partners` with `\"metadata\": { \"appUserId\": \"u_123\" }`. Values match as stored, so a number only matches a number. Two partners carrying the same value: `409 ambiguous_partner`. Storing the partner id on your user and calling the `{id}` form is quicker still.\n\nMints a link that signs the partner in to the partner portal, so your users go from your app to their referral dashboard without a second login. Call it from your backend when the user clicks through (a \"Refer & earn\" button that hits your server), and redirect the browser to `url` at once: it works **once**, for **five minutes**, and signs in whoever opens it. Don't email it, log it, or render it into a page ahead of time.\n\nThe link goes to your white-label domain when one is verified (`refer.example.com/auth/magic?…`), otherwise to the portal on OpenPartner's own domain. `returnTo` (a portal path such as `/links`) picks the page it lands on.\n\nThe partner must be one `POST /auth/signin` would email: active, not revoked, with portal access. Otherwise `409`: `partner_revoked`, `pending_review` (an applicant not yet approved), `portal_access_disabled` (turn it on with `PATCH /partners/{id}`), or `partner_not_activated` (invited, but hasn't accepted; partners created with `sendInvite: false` are active at once). The checks run again when the link is opened, along with the served-country check on the partner's own connection.\n\n**`partners:signin`** is the scope for this, and nothing else grants it, `partners:write` included. The OpenPartner Network's federation key is refused even if it holds the scope. Portal sessions get `403 api_key_required`: this is a server-to-server call. Each key may mint 600 links a minute, and each partner 10 (`429 rate_limited`), and every link minted is recorded with the key that asked for it.\n\nOn OpenPartner Cloud the brand needs an approved account (`403 brand_pending_review`).\n\n**Access:** admin key, or a scoped key with `partners:signin`.","security":[{"adminKey":[]},{"scopedKey":["partners:signin"]}],"x-openpartner-access":{"kind":"admin","scopes":["partners:signin"],"partner":false},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerSigninLinkLookupRequest"}}}},"responses":{"201":{"description":"Minted. Redirect to `url` now.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerSigninLink"}}}},"400":{"description":"`invalid_body`, `tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`, `api_key_required`, `brand_pending_review`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`partner_not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`ambiguous_partner`, `partner_revoked`, `pending_review`, `portal_access_disabled`, `partner_not_activated`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/partners/{id}/revoke":{"post":{"operationId":"revokePartner","tags":["Partners"],"summary":"Suspend a partner","description":"Ends the partner's sessions and partner keys and stops future attribution to them. Existing links keep redirecting (clicks are flagged `revoked`); past commissions are untouched.\n\nEmails the partner a suspension notice unless `notify: false`. For a partner without portal access the default is the other way round: they are emailed only with `notify: true`.\n\n**Access:** admin key, or a scoped key with `partners:write`.","security":[{"adminKey":[]},{"scopedKey":["partners:write"]}],"x-openpartner-access":{"kind":"admin","scopes":["partners:write"],"partner":false},"parameters":[{"schema":{"type":"string","description":"The id."},"required":true,"description":"The id.","name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RevokePartnerRequest"}}}},"responses":{"200":{"description":"Revoked.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"revokedAt":{"type":"string","format":"date-time","pattern":"Z$"},"notified":{"type":"boolean"},"notifyError":{"type":["string","null"]}},"required":["ok","revokedAt","notified","notifyError"]}}}},"400":{"description":"`invalid_body`, `tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`already_revoked`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/partners/{id}/reinstate":{"post":{"operationId":"reinstatePartner","tags":["Partners"],"summary":"Undo a revoke","description":"**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"parameters":[{"schema":{"type":"string","description":"The id."},"required":true,"description":"The id.","name":"id","in":"path"}],"responses":{"200":{"description":"Reinstated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Ok"}}}},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`not_revoked`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/partners/{id}/commission-snapshot":{"get":{"operationId":"getPartnerCommissionOverride","tags":["Partners"],"summary":"Get a partner's commission override","description":"**Access:** admin key, or the partner's own key (`:id` must be that partner). Scoped keys get `403 forbidden`.","security":[{"adminKey":[]},{"partnerKey":[]}],"x-openpartner-access":{"kind":"partnerOrAdmin","scopes":[],"partner":true},"parameters":[{"schema":{"type":"string","description":"The id."},"required":true,"description":"The id.","name":"id","in":"path"}],"responses":{"200":{"description":"The override, or null.","content":{"application/json":{"schema":{"type":"object","properties":{"snapshot":{"anyOf":[{"$ref":"#/components/schemas/CommissionSnapshot"},{"type":"null"}]}},"required":["snapshot"]}}}},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"put":{"operationId":"setPartnerCommissionOverride","tags":["Partners"],"summary":"Set a partner's commission override","description":"Replaces the program's commission rule for this partner's future commissions on every program that has no program-scoped terms for them (see `setPartnerProgramCommission`). Omitting `commissionRules` clears any compound rules.\n\n**The override replaces the whole rule set; it doesn't merge.** A program that pays $25 on `approved` and $400 on `loan_funded`, overridden with only the `loan_funded` rule, pays this partner nothing on `approved`. To change one stage, send the program's full `commissionRule` array with that one rule edited.\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"parameters":[{"schema":{"type":"string","description":"The id."},"required":true,"description":"The id.","name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommissionOverrideRequest"}}}},"responses":{"200":{"description":"Saved.","content":{"application/json":{"schema":{"type":"object","properties":{"snapshot":{"$ref":"#/components/schemas/CommissionSnapshot"}},"required":["snapshot"]}}}},"400":{"description":"`invalid_body`, `tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`partner_not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"operationId":"clearPartnerCommissionOverride","tags":["Partners"],"summary":"Clear a partner's commission override","description":"Clears the partner-wide terms only. Program-scoped terms stay.\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"parameters":[{"schema":{"type":"string","description":"The id."},"required":true,"description":"The id.","name":"id","in":"path"}],"responses":{"200":{"description":"Cleared.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Ok"}}}},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`partner_not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/partners/{id}/programs/{programId}/commission":{"get":{"operationId":"getPartnerProgramCommission","tags":["Partners"],"summary":"Get a partner's commission terms on one program","description":"**Access:** admin key, or the partner's own key (`:id` must be that partner). Scoped keys get `403 forbidden`.","security":[{"adminKey":[]},{"partnerKey":[]}],"x-openpartner-access":{"kind":"partnerOrAdmin","scopes":[],"partner":true},"parameters":[{"schema":{"type":"string","description":"The id."},"required":true,"description":"The id.","name":"id","in":"path"},{"schema":{"type":"string","description":"The programId."},"required":true,"description":"The programId.","name":"programId","in":"path"}],"responses":{"200":{"description":"The program-scoped terms, or null.","content":{"application/json":{"schema":{"type":"object","properties":{"snapshot":{"anyOf":[{"$ref":"#/components/schemas/CommissionSnapshot"},{"type":"null"}]}},"required":["snapshot"]}}}},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"put":{"operationId":"setPartnerProgramCommission","tags":["Partners"],"summary":"Set a partner's commission terms on one program","description":"Same body as `setPartnerCommissionOverride`, for one program. On that program these terms beat the partner-wide override and set the holdback too (omitted = the program's own). The partner doesn't need a grant on the program: terms can be agreed before access, and outlive a revoked grant.\n\n**These terms replace the whole rule set; they don't merge.** The engine uses this row's `commissionRules` instead of the partner-wide override and the program's `commissionRule`, so any stage the row leaves out pays this partner nothing. For example, a program pays $25 on `approved` and $400 on `loan_funded`. Terms holding only `{ \"eventType\": \"loan_funded\", \"type\": \"fixed\", \"value\": 500 }` pay $500 on `loan_funded` and **$0 on `approved`**.\n\nTo raise one stage, start from the rules the partner earns on today, change that one rule, and send the whole array. `GET /partners/{id}/programs` returns each program's `commissionRule` and this partner's program-scoped `commission`; `GET /partners/{id}/commission-snapshot` returns the partner-wide override.\n\n```json\n{ \"commissionType\": \"fixed\", \"commissionValue\": 25, \"recurring\": false,\n  \"commissionRules\": [\n    { \"trigger\": \"every\", \"eventType\": \"approved\",    \"type\": \"fixed\", \"value\": 25 },\n    { \"trigger\": \"every\", \"eventType\": \"loan_funded\", \"type\": \"fixed\", \"value\": 500 }\n  ] }\n```\n\nThe legacy `commissionType`/`commissionValue`/`recurring` columns are still required. When `commissionRules` is set the engine ignores them, so fill them from one of the rules.\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"parameters":[{"schema":{"type":"string","description":"The id."},"required":true,"description":"The id.","name":"id","in":"path"},{"schema":{"type":"string","description":"The programId."},"required":true,"description":"The programId.","name":"programId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommissionOverrideRequest"}}}},"responses":{"200":{"description":"Saved.","content":{"application/json":{"schema":{"type":"object","properties":{"snapshot":{"$ref":"#/components/schemas/CommissionSnapshot"}},"required":["snapshot"]}}}},"400":{"description":"`invalid_body`, `tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`partner_not_found`, `program_not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"operationId":"clearPartnerProgramCommission","tags":["Partners"],"summary":"Clear a partner's commission terms on one program","description":"Commissions on the program fall back to the partner-wide override, or the program's rule.\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"parameters":[{"schema":{"type":"string","description":"The id."},"required":true,"description":"The id.","name":"id","in":"path"},{"schema":{"type":"string","description":"The programId."},"required":true,"description":"The programId.","name":"programId","in":"path"}],"responses":{"200":{"description":"Cleared.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Ok"}}}},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`partner_not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/programs":{"get":{"operationId":"listPrograms","tags":["Programs"],"summary":"List programs","description":"**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"responses":{"200":{"description":"All programs, newest first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProgramList"}}}},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"createProgram","tags":["Programs"],"summary":"Create a program","description":"A program is one set of terms: commission rule, attribution window and model, destination URL.\n\n`shareOnNetwork` defaults to **true** when the tenant is on the OpenPartner Network, which lists the program on the public marketplace and makes `marketplaceDescription` required. Pass `shareOnNetwork: false` for a private program such as customer referral.\n\n`holdbackDays` above 0 means commissions are **approved automatically** that many days after they accrue (by the scheduled jobs), and then paid by the next payout run. 0 or omitted means every commission waits for `POST /commissions/{id}/approve`.\n\n`attributionWindowDays` decides which click wins a customer's first conversion of each event type. With `renewalsStayWithPartner` (default true), repeats of that event type, such as monthly `invoice_paid` renewals, keep the same partners and weights for as long as the customer pays, even when another partner's link is clicked later. Past the window, only sub-rules with `recurring: true` pay on them, up to `recurringMonths`.\n\n**Destination and the click id.** The router appends `?cref=<click id>` to `destinationUrl`. To put the id in a parameter of your own instead, write the `{click_id}` macro where it goes, exactly like that (`%7Bclick_id%7D` works too). It may go in the path, query or fragment, not the host. Any other brace text, such as `{{click_id}}`, `{ click_id }`, `{CLICK_ID}` or `{subid}`, is refused with `400`, because the router would send it through as text. A Stripe Payment Link (`buy.stripe.com`) reads the referral only from `client_reference_id`: without `?client_reference_id={click_id}` the program still saves, with a `warnings` entry.\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProgramRequest"}}}},"responses":{"201":{"description":"Created. `warnings` flags a destination that will probably lose referrals.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProgramSaved"}}}},"400":{"description":"`invalid_body`, `tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"`trial_expired`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/programs/stats":{"get":{"operationId":"getProgramStats","tags":["Programs"],"summary":"Each program's results over a window","description":"Clicks, partners, conversions, attributed revenue and commission by status for every program, over [`since`, `until`). The default window is the 30 days before `until`, which defaults to now.\n\n- A click, conversion or commission counts toward the program it was made in. A bonus's commission counts toward the bonus's program; a bonus with no program counts toward none.\n- `partners` counts the partners enrolled in the program now, plus any with clicks, conversions or commission in it during the window, such as a partner removed from the program after earning in it.\n- Amounts are per currency, never converted or added together. Commission is by accrual date, in its status today.\n\n`since` at or after `until` answers `400 invalid_window`.\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"parameters":[{"schema":{"type":"string","format":"date-time","description":"ISO-8601 start of the window (UTC, `Z`), inclusive. Default: 30 days before `until`.","pattern":"Z$"},"required":false,"description":"ISO-8601 start of the window (UTC, `Z`), inclusive. Default: 30 days before `until`.","name":"since","in":"query"},{"schema":{"type":"string","format":"date-time","description":"ISO-8601 end of the window (UTC, `Z`), exclusive. Default: now.","pattern":"Z$"},"required":false,"description":"ISO-8601 end of the window (UTC, `Z`), exclusive. Default: now.","name":"until","in":"query"}],"responses":{"200":{"description":"One row per program.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProgramStatsList"}}}},"400":{"description":"`invalid_query`, `tenant_slug_missing`, `invalid_window`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/me/programs":{"get":{"operationId":"listMyPrograms","tags":["Programs"],"summary":"Programs the caller can create links for","description":"A partner sees its granted programs that are currently active. An admin sees every program. Commission terms aren't included.\n\n**Access:** admin key or a partner key. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]},{"partnerKey":[]}],"x-openpartner-access":{"kind":"handler","scopes":[],"partner":true},"responses":{"200":{"description":"Programs.","content":{"application/json":{"schema":{"type":"object","properties":{"programs":{"type":"array","items":{"$ref":"#/components/schemas/PartnerVisibleProgram"}}},"required":["programs"]}}}},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/programs/{id}":{"patch":{"operationId":"updateProgram","tags":["Programs"],"summary":"Update a program","description":"Changes apply to future commissions. Partners with a commission override keep it. Changing `attributionWindowDays` or `renewalsStayWithPartner` decides events attributed from then on; renewals of customers already won keep their partner whatever the new window.\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"parameters":[{"schema":{"type":"string","description":"The id."},"required":true,"description":"The id.","name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateProgramRequest"}}}},"responses":{"200":{"description":"The updated program. `warnings` flags a destination that will probably lose referrals.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProgramSaved"}}}},"400":{"description":"`invalid_body`, `tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/partners/{id}/programs":{"get":{"operationId":"listPartnerPrograms","tags":["Program assignments"],"summary":"List programs with this partner's grant state","description":"**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"parameters":[{"schema":{"type":"string","description":"The id."},"required":true,"description":"The id.","name":"id","in":"path"}],"responses":{"200":{"description":"Every program, flagged granted or not.","content":{"application/json":{"schema":{"type":"object","properties":{"programs":{"type":"array","items":{"$ref":"#/components/schemas/ProgramGrant"}}},"required":["programs"]}}}},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`partner_not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"grantPartnerPrograms","tags":["Program assignments"],"summary":"Grant programs to a partner","description":"Idempotent. Unknown program ids are skipped. Mints a coupon per newly granted program.\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"parameters":[{"schema":{"type":"string","description":"The id."},"required":true,"description":"The id.","name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GrantProgramsRequest"}}}},"responses":{"201":{"description":"The program ids granted.","content":{"application/json":{"schema":{"type":"object","properties":{"added":{"type":"array","items":{"type":"string"}}},"required":["added"]}}}},"400":{"description":"`invalid_body`, `tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`partner_not_found`, `no_valid_campaigns`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/partners/{id}/programs/{programId}":{"delete":{"operationId":"revokePartnerProgram","tags":["Program assignments"],"summary":"Revoke a program grant","description":"Blocks new links on that program and retires the partner's coupons on it. Existing links keep redirecting.\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"parameters":[{"schema":{"type":"string","description":"The id."},"required":true,"description":"The id.","name":"id","in":"path"},{"schema":{"type":"string","description":"The programId."},"required":true,"description":"The programId.","name":"programId","in":"path"}],"responses":{"200":{"description":"Revoked.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"retiredCoupons":{"type":"array","items":{"type":"string"}}},"required":["ok","retiredCoupons"]}}}},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`grant_not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/partners/{id}/links":{"get":{"operationId":"listPartnerLinks","tags":["Links"],"summary":"List a partner's links","description":"**Access:** admin key, a scoped key with `links:read`, or the partner's own key (`:id` must be that partner).","security":[{"adminKey":[]},{"partnerKey":[]},{"scopedKey":["links:read"]}],"x-openpartner-access":{"kind":"partnerOrAdmin","scopes":["links:read"],"partner":true},"parameters":[{"schema":{"type":"string","description":"The id."},"required":true,"description":"The id.","name":"id","in":"path"}],"responses":{"200":{"description":"Newest first.","content":{"application/json":{"schema":{"type":"object","properties":{"links":{"type":"array","items":{"$ref":"#/components/schemas/Link"}}},"required":["links"]}}}},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"createPartnerLink","tags":["Links"],"summary":"Create a share link","description":"`linkKey` is the short code in the share URL and is unique per tenant. A partner caller must hold a grant for the program; admin and scoped callers don't. `destinationUrl` overrides the program's destination only when the program allows deep links to that host.\n\n**Access:** admin key, a scoped key with `links:write`, or the partner's own key (`:id` must be that partner).","security":[{"adminKey":[]},{"partnerKey":[]},{"scopedKey":["links:write"]}],"x-openpartner-access":{"kind":"partnerOrAdmin","scopes":["links:write"],"partner":true},"parameters":[{"schema":{"type":"string","description":"The id."},"required":true,"description":"The id.","name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateLinkRequest"}}}},"responses":{"201":{"description":"Created. `warnings` flags a destination override that will probably lose referrals.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkSaved"}}}},"400":{"description":"`invalid_body`, `tenant_slug_missing`, `destination_override_not_allowed`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`, `campaign_not_granted`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`partner_not_found`, `campaign_not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`linkKey_taken`, `campaign_not_active`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/attribution/identify":{"post":{"operationId":"identify","tags":["Attribution"],"summary":"Stitch a click to a user","description":"Links the click id (`cref`) a visitor arrived with to your user id. The browser SDK calls this on signup and login; your backend can call it instead if it receives the `cref`.\n\nRepeat calls for the same pair are no-ops. Events already recorded for the user that have not attributed yet are attributed when the stitch lands (`backfilledAttributions`); an event that already attributed keeps its credit, and refunds and other corrective events are never attributed. Calling it on login too stitches a user who clicked on another device.\n\nUnauthenticated by design. It accepts any origin (CORS) and is limited to 120 requests per minute per client IP. Use the same `userId` you later send to `POST /attribution/events`.\n\n**Access:** public, no API key. Rate-limited per client IP.","security":[],"x-openpartner-access":{"kind":"public","scopes":[],"partner":false},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentifyRequest"}}}},"responses":{"200":{"description":"Stitched (or already stitched).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentifyResult"}}}},"400":{"description":"`invalid_body`, `tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`click_not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/attribution/landing":{"post":{"operationId":"reportLanding","tags":["Attribution"],"summary":"Report that a click arrived on your site","description":"The browser SDK calls this once per click, when it first captures the click id from the landing URL. Admin → Tracking health compares landings with clicks, so clicks lost between the partner link and your page show up: a redirect that dropped `?cref=`, a page without the script, a consent banner holding it back.\n\nOnly the first call for a click records anything (`firstLanding`). The page's hostname is taken from the browser's `Origin` header (or `Referer`), never its path or query. A click id this program doesn't have is a `404` and is counted on Tracking health, since it usually means the script's API URL names another program.\n\nUnauthenticated by design, like identify: any origin (CORS), 120 requests per minute per client IP. Turn it off in the SDK with `reportLanding: false` (script tag: `data-landing=\"off\"`).\n\n**Access:** public, no API key. Rate-limited per client IP.","security":[],"x-openpartner-access":{"kind":"public","scopes":[],"partner":false},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LandingRequest"}}}},"responses":{"200":{"description":"Recorded (or already recorded).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LandingResult"}}}},"400":{"description":"`invalid_body`, `tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`click_not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/attribution/events":{"post":{"operationId":"recordEvent","tags":["Attribution"],"summary":"Record a conversion event","description":"Server-to-server conversion ingest. The event is attributed to the partner whose click the user was stitched to (within the program's window and model) and commissions accrue.\n\n**Renewals.** Send each renewal as its own event with the same `userId` and `type` (one `invoice_paid` per payment). Once the customer has an attributed event of that type, each later one keeps that event's partners and weights, however old the click, while the program has `renewalsStayWithPartner` on (the default).\n\n**Event types.** Any string is accepted. Two have built-in meaning:\n- `signup` is the funnel's *signups* stage, as are `trial_started`, `subscription_created` and `invoice_paid` (`signup` and `trial_started` are *leads* in the timeseries).\n- `invoice_paid` is the funnel's *paid* stage, and on OpenPartner Cloud it is billable GMV, as is `subscription_created`. Report payments as `invoice_paid`. Other types with a value still accrue commission but are not counted as GMV.\n\n`value` is a JSON number in major units (`49.99`), not a string. Omit it for an event with no value; `null` is refused. `currency` is an ISO 4217 code in any case, stored upper case (`inr` is `INR`). Without one the event is stored in the brand's default currency (Settings → Program; USD unless changed), so send it whenever the brand takes more than one currency.\n\n**Idempotency.** Always send `externalEventId`: your system's stable id for this one event (invoice id, order id, deal id + stage). A retry with the same key returns `200` with `replayed: true` and the original result, writing nothing. Reusing a key for a different userId, type, value or currency returns `409 external_event_id_conflict`. Keys are per tenant, up to 120 characters, and shared with `POST /coupons/redeem`.\n\n**Refunds.** Report them to `POST /attribution/refunds`. A `refund` or `chargeback` event sent here is recorded but never attributed (`attribution.status` `corrective`) and reverses nothing.\n\n**Don't report a payment twice.** If your Stripe account also sends webhooks to this deployment (`/webhooks/stripe`, single-tenant self-host), those payments are already recorded: invoices keyed by the Stripe event id, one-time Checkout payments by the Checkout Session id. Reporting the same payment here under your own invoice id records it a second time. Checkout Session ids are reserved for the webhook there: one (`cs_live_…`, `cs_test_…`) as the key returns `409 recorded_from_stripe`.\n\n**Stripe connected (OpenPartner Cloud).** Once your Stripe account is connected through the OpenPartner Stripe App, Stripe invoices and Checkout payments are recorded from Stripe itself, keyed by the invoice or Checkout Session id. An `externalEventId` that is one of those ids (`in_…`, `cs_live_…`, `cs_test_…`) returns `409 recorded_from_stripe`: Stripe's record of the payment wins.\n\n**Access:** admin key, or a scoped key with `events:write`.","security":[{"adminKey":[]},{"scopedKey":["events:write"]}],"x-openpartner-access":{"kind":"admin","scopes":["events:write"],"partner":false},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordEventRequest"}}}},"responses":{"200":{"description":"Recorded (or replayed).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventResult"}}}},"400":{"description":"`invalid_body`, `tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`external_event_id_conflict`, `recorded_from_stripe`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/attribution/conversions":{"post":{"operationId":"recordConversion","tags":["Attribution"],"summary":"Record a conversion against a click","description":"For senders that hold a click id but never saw an identify: a lender reporting a funded loan, a CRM, an affiliate network. The click id reaches them through the destination URL's `{click_id}` macro, or the `cref` the router appends. One call stitches the click to a user and records the event, in one transaction.\n\nSend `userId` when you have your own id for the user: attribution then follows the program's model across every click that user is stitched to, as on `POST /attribution/events`. Without it the click's own synthetic user is used, and this click alone is credited. Events recorded for the user before this stitch are not backfilled; `POST /attribution/identify` does that.\n\n`value` and `currency` work as on `POST /attribution/events`: without a currency the event is stored in the brand's default currency.\n\n**Scope.** Conversions are scoped to the tenant, not to a program. The click id already says which program the click came through, so there's no `programId` to send, and a click from any program in this tenant is accepted. A click id this tenant doesn't have is `404 click_not_found`. Keys with `events:write` (and the postback token) work across every program in the tenant, so a sender holding one can report against any of its clicks.\n\n**Idempotency.** `externalEventId` is required. A retry with the same key returns `200` with `replayed: true` and the original result, even if the click has been flagged since. Keys are unique across the **whole tenant**: one namespace shared by `POST /attribution/events`, `GET /postback` (`txn_id`), `POST /coupons/redeem` and the Stripe webhook. Reusing a key for a different click, userId, type, value or currency returns `409 external_event_id_conflict`, and that conversion is not recorded. With your Stripe account connected, a Stripe invoice or Checkout Session id as the key returns `409 recorded_from_stripe`, as on `POST /attribution/events`, and so does a Checkout Session id on a single-tenant self-host whose Stripe webhook points at `/webhooks/stripe`. So:\n- **One key per stage.** A loan reported as `approved` and later `loan_funded` needs two keys, e.g. `loan-881:approved` and `loan-881:funded`.\n- **Prefix keys per sender.** Two suppliers' order ids can collide: `acme:loan-881:funded`, `zenlend:loan-881:funded`.\n\n**Outcome.** Every `200` carries `outcome` (and sometimes `reason`), which says what the conversion came to: `attributed`, `no_matching_rule` (credited, but no commission rule takes this `type`, which usually means a typo'd stage name) or `not_attributed` (e.g. `outside_window`). The conversion is recorded in every case, so don't retry on these. Fix the cause and send the next conversion.\n\nA click flagged by velocity limits or a revoked partner earns nothing: `422 click_flagged`.\n\n**Access:** admin key, or a scoped key with `events:write`.","security":[{"adminKey":[]},{"scopedKey":["events:write"]}],"x-openpartner-access":{"kind":"admin","scopes":["events:write"],"partner":false},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordConversionRequest"}}}},"responses":{"200":{"description":"Recorded (or replayed).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversionResult"}}}},"400":{"description":"`invalid_body`, `tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`click_not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`external_event_id_conflict`, `recorded_from_stripe`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"`click_flagged`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/postback":{"get":{"operationId":"conversionPostback","tags":["Attribution"],"summary":"Record a conversion from a postback URL","description":"`POST /attribution/conversions` for senders that can only fire a URL: affiliate networks, many lenders, tracking pixels. Same result: the click is stitched to a user and the event recorded, in one transaction.\n\n```\nGET /postback?click_id={click_id}&type=sale&amount=49.00&currency=USD&txn_id={transaction_id}&token=opcp_…\n```\n\n**The token is in the URL.** Proxies, browser history and the sender's own logs keep query strings, so treat a postback URL as leaked from the day you hand it out, and use `POST /attribution/conversions` with a Bearer key whenever the sender can set a header. The postback token is built for this: it records conversions and nothing else, is never accepted as a Bearer key (and an API key is never accepted as `token`), can be pinned to the sender's IPs (`ipAllowlist`), and rotating it on the Integrations page stops the old URL at once. OpenPartner's own request log redacts it. Always use `https://`.\n\n**One token per tenant.** The token records conversions for every program in the tenant, so any sender holding it can report against any click. When several suppliers or networks send postbacks, pin the token to their addresses with `ipAllowlist` (`PATCH /conversion-postback`), and rotate it when one leaves.\n\n**Idempotency.** `txn_id` is the `externalEventId`: a network re-firing the postback records once. Keys are unique across the whole tenant, in one namespace with `POST /attribution/conversions`, `POST /attribution/events`, `POST /coupons/redeem` and the Stripe webhook. Reusing one for a different conversion is `409 external_event_id_conflict`, and that conversion is not recorded. Send a different `txn_id` for each stage (`loan-881:approved`, `loan-881:funded`), prefixed per sender.\n\n**Responses are plain text**, not JSON. A `200` is `ok` followed by what the conversion came to:\n\n| Body | Meaning |\n| --- | --- |\n| `ok attributed` | A partner was credited and earned commission. |\n| `ok attributed no_commission` | Credited, but nothing accrued: the program has ended, a `first` rule already paid, a cap was reached, the amount was zero, or it's a renewal past the attribution window, which only `recurring` rules pay. |\n| `ok no_matching_rule` | Credited, but no commission rule takes this `type`. Usually a typo'd stage name. Admin → Integrations lists these types. |\n| `ok not_attributed outside_window` | No partner credited: the click is older than the program's attribution window. |\n| `ok not_attributed no_click` | No partner credited: the partner was revoked. |\n| `ok not_attributed` | A replay of a conversion that wasn't attributed the first time. |\n\nAll of these are `200` because the conversion is recorded: a retry would only replay it. Match on the `ok` prefix to tell success from an error. Errors are the bare code (`invalid_parameters` is followed by the offending names, e.g. `invalid_parameters: click_id, txn_id`). Every response is `Cache-Control: no-store`. `HEAD` answers `405` and records nothing, so a link checker can't create a conversion.\n\nThe token is checked before anything else, so a caller without one learns nothing about the parameters or clicks. Mint it under **Admin → Integrations** (or `POST /conversion-postback/token`).\n\n**Access:** the conversion postback token in the `token` query parameter. API keys are not accepted, as a Bearer header or as `token`.","security":[{"postbackToken":[]}],"x-openpartner-access":{"kind":"postbackToken","scopes":[],"partner":false},"parameters":[{"schema":{"type":"string","description":"The conversion postback token, `opcp_…`. Required."},"required":true,"description":"The conversion postback token, `opcp_…`. Required.","name":"token","in":"query"},{"schema":{"type":"string","maxLength":64,"description":"The click id, from the destination URL's `{click_id}` macro. Alias: `cref`. Required."},"required":true,"description":"The click id, from the destination URL's `{click_id}` macro. Alias: `cref`. Required.","name":"click_id","in":"query"},{"schema":{"type":"string","maxLength":255,"description":"Event type, e.g. `sale`, `loan_funded`. Required."},"required":true,"description":"Event type, e.g. `sale`, `loan_funded`. Required.","name":"type","in":"query"},{"schema":{"type":"string","maxLength":120,"description":"The sender's transaction id: the idempotency key (`externalEventId`). Alias: `transaction_id`. Required."},"required":true,"description":"The sender's transaction id: the idempotency key (`externalEventId`). Alias: `transaction_id`. Required.","name":"txn_id","in":"query"},{"schema":{"type":"string","description":"Decimal value, e.g. `49.00`. Alias: `value`. Empty (an unfilled macro) means no amount."},"required":false,"description":"Decimal value, e.g. `49.00`. Alias: `value`. Empty (an unfilled macro) means no amount.","name":"amount","in":"query"},{"schema":{"type":"string","minLength":3,"maxLength":3,"description":"ISO 4217 code."},"required":false,"description":"ISO 4217 code.","name":"currency","in":"query"},{"schema":{"type":"string","maxLength":255,"description":"Your own id for the user, as on `POST /attribution/conversions`."},"required":false,"description":"Your own id for the user, as on `POST /attribution/conversions`.","name":"user_id","in":"query"},{"schema":{"type":"string","enum":["1"],"description":"`1` answers a 1×1 transparent GIF instead of `ok`, for senders that fire the postback as an image."},"required":false,"description":"`1` answers a 1×1 transparent GIF instead of `ok`, for senders that fire the postback as an image.","name":"pixel","in":"query"}],"responses":{"200":{"description":"Recorded (or replayed): `ok` and the outcome, or a 1×1 GIF with `pixel=1`.","content":{"text/plain":{"schema":{"type":"string","enum":["ok attributed","ok attributed no_commission","ok no_matching_rule","ok not_attributed","ok not_attributed outside_window","ok not_attributed no_click","ok not_attributed no_identity","ok not_attributed corrective"],"example":"ok attributed"}},"image/gif":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"`tenant_slug_missing`, `repeated_parameter`, `invalid_parameters`","content":{"text/plain":{"schema":{"type":"string","description":"The error code, e.g. `invalid_token`."}}}},"401":{"description":"`missing_token`, `invalid_token`","content":{"text/plain":{"schema":{"type":"string","description":"The error code, e.g. `invalid_token`."}}}},"403":{"description":"`ip_not_allowed`","content":{"text/plain":{"schema":{"type":"string","description":"The error code, e.g. `invalid_token`."}}}},"404":{"description":"`click_not_found`","content":{"text/plain":{"schema":{"type":"string","description":"The error code, e.g. `invalid_token`."}}}},"409":{"description":"`external_event_id_conflict`","content":{"text/plain":{"schema":{"type":"string","description":"The error code, e.g. `invalid_token`."}}}},"422":{"description":"`click_flagged`","content":{"text/plain":{"schema":{"type":"string","description":"The error code, e.g. `invalid_token`."}}}}}}},"/conversion-postback":{"get":{"operationId":"getConversionPostbackToken","tags":["Attribution"],"summary":"Get the conversion postback token","description":"The live postback token's prefix, IP allowlist and last use, or `null` when postbacks are off. The token itself is never shown again after it's minted.\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"responses":{"200":{"description":"The live token, or null.","content":{"application/json":{"schema":{"type":"object","properties":{"token":{"anyOf":[{"$ref":"#/components/schemas/PostbackToken"},{"type":"null"}]}},"required":["token"]}}}},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"operationId":"updateConversionPostbackAllowlist","tags":["Attribution"],"summary":"Set the postback token's IP allowlist","description":"Replaces the allowlist on the live token without rotating it, so URLs already handed out keep working. `[]` accepts any address.\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePostbackAllowlistRequest"}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"type":"object","properties":{"token":{"$ref":"#/components/schemas/PostbackToken"}},"required":["token"]}}}},"400":{"description":"`invalid_body`, `tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`no_postback_token`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/conversion-postback/token":{"post":{"operationId":"rotateConversionPostbackToken","tags":["Attribution"],"summary":"Mint or rotate the conversion postback token","description":"Mints the token `GET /postback` authenticates with, and returns it **once** as `plaintext`. A tenant has one live token: minting again revokes the old one immediately, so every URL carrying it stops working. Do that when a sender leaves, or when a URL may have leaked.\n\nThe token covers every program in the tenant, so a sender holding it can report against any click. `ipAllowlist` limits where it's accepted from: IP addresses or CIDR ranges, matched against the client address the API sees through one trusted proxy hop. Omitted, the previous token's allowlist carries over.\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RotatePostbackTokenRequest"}}}},"responses":{"201":{"description":"Minted. `plaintext` is shown only here.","content":{"application/json":{"schema":{"type":"object","properties":{"plaintext":{"type":"string","example":"opcp_…"},"token":{"$ref":"#/components/schemas/PostbackToken"}},"required":["plaintext","token"]}}}},"400":{"description":"`invalid_body`, `tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"operationId":"revokeConversionPostbackToken","tags":["Attribution"],"summary":"Turn postbacks off","description":"Revokes the live token without minting another. `GET /postback` then answers `401 invalid_token`.\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"responses":{"200":{"description":"`revoked` is false when there was no live token.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"revoked":{"type":"boolean"}},"required":["ok","revoked"]}}}},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/attribution/unmatched-event-types":{"get":{"operationId":"listUnmatchedEventTypes","tags":["Attribution"],"summary":"Event types that matched no commission rule","description":"Event types from the last 30 days that were credited to a partner but that no commission rule on the partner's terms takes, so the partner earned nothing. A sender reporting `funded` when the program pays on `loan_funded` shows up here. Admin → Integrations shows the same list as a warning.\n\nChecked against the rules as they are now: adding a rule for the type, or fixing the sender, clears the entry for later events. OpenPartner's own types (`signup`, `trial_started`, `subscription_created`, `invoice_paid`, `refund`, `dispute_created`, `invoice_payment_failed`) are left out, because a program that pays only on payments is meant to pay nothing on signups. Newest first.\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"responses":{"200":{"description":"Newest first; empty when every type matched a rule.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnmatchedEventTypes"}}}},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/attribution/refunds":{"post":{"operationId":"recordRefund","tags":["Attribution"],"summary":"Report a refund or chargeback","description":"Reports that a conversion was refunded, in full or in part, or charged back. Reference the conversion by the `externalEventId` you sent with it (`conversionExternalEventId`, preferred) or by the `eventId` OpenPartner returned (`conversionEventId`): exactly one. Conversions recorded from Stripe, by the self-host Stripe webhook or, on OpenPartner Cloud, the OpenPartner Stripe App, can be referenced too, but their Stripe refunds are already handled there; don't report those twice.\n\nThe refund is recorded as a corrective event (type `refund` or `chargeback`, a negative `value`) that is never attributed, and the conversion's commissions follow it. Each commission ends up worth its original amount times the share of the conversion the customer kept:\n- **Not yet paid** (`accrued`, `approved`): reversed, firing `commission.reversed` (reason `refund` or `chargeback`) and the partner's postback. On a partial refund a replacement commission for the kept amount follows (`commission.accrued` with `replacesCommissionId`), keeping the original's status and accrual date, so its holdback doesn't restart.\n- **Already paid**: it stays paid. A negative adjustment records the refunded revenue the partner holds (`clawback_recorded`). OpenPartner deducts it from the partner's next manual or self-hosted Stripe Connect payout; a partner who owes more than they are due is not paid until enough accrues. Funded Stripe Connect payouts on hosted OpenPartner don't deduct it: recover those with the partner.\n- **In a payout right now**: left alone (`held_in_transfer`). Retry the same request once the payout settles.\n\n`amount` is in major units and defaults to everything not yet refunded. Partial refunds add up: two refunds of 30 and 20 on a 100 conversion leave each commission at 50%. A conversion with no `value` can only be refunded in full. A `chargeback` reverses commissions like a refund, so report it once it is final, not when a dispute opens.\n\n**Idempotency.** `externalEventId` is required: your id for this refund (refund id, credit note id). A retry with the same key returns `200` with `replayed: true`, records nothing new, and re-applies the refund, which also finishes any commission that was held in transfer. Reusing a key for a different conversion, kind or amount returns `409 external_event_id_conflict`. Keys share one namespace with `POST /attribution/events`.\n\n**Access:** admin key, or a scoped key with `events:write`.","security":[{"adminKey":[]},{"scopedKey":["events:write"]}],"x-openpartner-access":{"kind":"admin","scopes":["events:write"],"partner":false},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordRefundRequest"}}}},"responses":{"200":{"description":"Recorded (or replayed).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefundResult"}}}},"400":{"description":"`invalid_body`, `tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`conversion_not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`external_event_id_conflict`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"`not_a_conversion`, `currency_mismatch`, `partial_refund_needs_value`, `refund_exceeds_remaining`, `nothing_to_refund`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/clicks":{"post":{"operationId":"ingestClick","tags":["Attribution"],"summary":"Ingest a click recorded elsewhere","description":"For callers that run their own redirect (the OpenPartner Network's creator domains use it). Partner and program are derived from `linkKey`, never trusted from the caller. Idempotent on `id`. Most integrations don't need this: the click router records clicks itself.\n\n**Access:** admin key, or a scoped key with `clicks:write`.","security":[{"adminKey":[]},{"scopedKey":["clicks:write"]}],"x-openpartner-access":{"kind":"admin","scopes":["clicks:write"],"partner":false},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestClickRequest"}}}},"responses":{"200":{"description":"Already recorded (`replayed: true`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClickIngestResult"}}}},"201":{"description":"Recorded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClickIngestResult"}}}},"400":{"description":"`invalid_body`, `tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`link_not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/partners/{id}/coupons":{"get":{"operationId":"listPartnerCoupons","tags":["Coupons"],"summary":"List a partner's coupon codes","description":"Includes deactivated codes, with 90-day redemption counts and revenue.\n\n**Access:** admin key, or the partner's own key (`:id` must be that partner). Scoped keys get `403 forbidden`.","security":[{"adminKey":[]},{"partnerKey":[]}],"x-openpartner-access":{"kind":"partnerOrAdmin","scopes":[],"partner":true},"parameters":[{"schema":{"type":"string","description":"The id."},"required":true,"description":"The id.","name":"id","in":"path"}],"responses":{"200":{"description":"Oldest first.","content":{"application/json":{"schema":{"type":"object","properties":{"coupons":{"type":"array","items":{"$ref":"#/components/schemas/CouponWithStats"}}},"required":["coupons"]}}}},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"createPartnerCoupon","tags":["Coupons"],"summary":"Mint a coupon code","description":"Admins can mint any code. Partners and `partners:write` keys can mint only on programs with `partnersMayCustomizeCode`, up to 5 active and 10 per day per program, and never a code that already exists in Stripe. `code` defaults to a generated one.\n\n**Access:** admin key; the partner's own key when the program has `partnersMayCustomizeCode` and the partner holds a grant; or a scoped key with `partners:write` when the program has `partnersMayCustomizeCode`.","security":[{"adminKey":[]},{"partnerKey":[]},{"scopedKey":["partners:write"]}],"x-openpartner-access":{"kind":"handler","scopes":["partners:write"],"partner":true},"parameters":[{"schema":{"type":"string","description":"The id."},"required":true,"description":"The id.","name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCouponRequest"}}}},"responses":{"201":{"description":"Minted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Coupon"}}}},"400":{"description":"`invalid_body`, `tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"`trial_expired`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`, `partner_customization_disabled`, `campaign_not_granted`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`partner_not_found`, `program_not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`verification_required`, `active_codes_cap_reached`, `code_taken`, `campaign_not_active`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`daily_code_limit_reached`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"`code_check_unavailable`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/partners/{id}/coupons/{couponId}":{"delete":{"operationId":"deactivatePartnerCoupon","tags":["Coupons"],"summary":"Deactivate a coupon code","description":"Idempotent. Users the code already stitched keep attributing; new redemptions of it stop.\n\n**Access:** same as minting: admin key, or the partner / a `partners:write` key when the program has `partnersMayCustomizeCode`.","security":[{"adminKey":[]},{"partnerKey":[]},{"scopedKey":["partners:write"]}],"x-openpartner-access":{"kind":"handler","scopes":["partners:write"],"partner":true},"parameters":[{"schema":{"type":"string","description":"The id."},"required":true,"description":"The id.","name":"id","in":"path"},{"schema":{"type":"string","description":"The couponId."},"required":true,"description":"The couponId.","name":"couponId","in":"path"}],"responses":{"200":{"description":"The deactivated coupon.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Coupon"}}}},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`, `partner_customization_disabled`, `campaign_not_granted`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`coupon_not_found`, `program_not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/coupons/redeem":{"post":{"operationId":"redeemCoupon","tags":["Coupons"],"summary":"Record a coupon redemption","description":"Call from your checkout when a customer uses a partner code. Records the conversion and stitches the user to the partner, so their later events attribute too. `externalEventId` is required and works as on `POST /attribution/events` (same namespace), as do `value` and `currency`: without a currency the redemption is stored in the brand's default currency. With your Stripe account connected, codes entered at Stripe Checkout are redeemed from Stripe, and a Stripe invoice or Checkout Session id as the key returns `409 recorded_from_stripe`. The same goes for a Checkout Session id on a single-tenant self-host whose Stripe webhook points at `/webhooks/stripe`: the webhook records a one-time Checkout's payment under it, and redeems a code entered at Stripe Checkout itself. Redeem a code your own app took under your own order id, when the customer applies it and before they pay: a redemption made after the payment isn't credited with it.\n\n**Access:** admin key, or a scoped key with `events:write`.","security":[{"adminKey":[]},{"scopedKey":["events:write"]}],"x-openpartner-access":{"kind":"admin","scopes":["events:write"],"partner":false},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RedeemCouponRequest"}}}},"responses":{"200":{"description":"Replayed (`replayed: true`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CouponRedeemResult"}}}},"201":{"description":"Recorded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CouponRedeemResult"}}}},"400":{"description":"`invalid_body`, `tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`coupon_not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`external_event_id_conflict`, `recorded_from_stripe`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/partners/{id}/commissions":{"get":{"operationId":"listPartnerCommissions","tags":["Commissions"],"summary":"List a partner's commissions","description":"Newest accrual first, keyset-paginated: pass `nextCursor` back as `cursor`. Lifecycle: `accrued` → `approved` → `paid`, or `reversed`.\n\n**Access:** admin key, a scoped key with `commissions:read`, or the partner's own key (`:id` must be that partner).","security":[{"adminKey":[]},{"partnerKey":[]},{"scopedKey":["commissions:read"]}],"x-openpartner-access":{"kind":"partnerOrAdmin","scopes":["commissions:read"],"partner":true},"parameters":[{"schema":{"type":"string","description":"The id."},"required":true,"description":"The id.","name":"id","in":"path"},{"schema":{"type":"string","enum":["accrued","approved","paid","reversed"]},"required":false,"name":"status","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":500,"default":100},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string"},"required":false,"name":"cursor","in":"query"}],"responses":{"200":{"description":"One page.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommissionPage"}}}},"400":{"description":"`invalid_query`, `tenant_slug_missing`, `invalid_cursor`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/commissions":{"get":{"operationId":"listCommissions","tags":["Commissions"],"summary":"List all commissions","description":"Newest accrual first, keyset-paginated: pass `nextCursor` back as `cursor`. Lifecycle: `accrued` → `approved` → `paid`, or `reversed`.\n\n**Access:** admin key, or a scoped key with `commissions:read`.","security":[{"adminKey":[]},{"scopedKey":["commissions:read"]}],"x-openpartner-access":{"kind":"admin","scopes":["commissions:read"],"partner":false},"parameters":[{"schema":{"type":"string","enum":["accrued","approved","paid","reversed"]},"required":false,"name":"status","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":500,"default":100},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string"},"required":false,"name":"cursor","in":"query"}],"responses":{"200":{"description":"One page.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommissionPage"}}}},"400":{"description":"`invalid_query`, `tenant_slug_missing`, `invalid_cursor`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/commissions/{id}/approve":{"post":{"operationId":"approveCommission","tags":["Commissions"],"summary":"Approve an accrued commission","description":"Refused while the holdback is running. Fires `commission.approved`. With a holdback above 0 the scheduled jobs approve matured commissions on their own; with none, this is the only way they're approved.\n\nA recruiter's share of this commission (two-tier, `sourceCommissionId`) is approved with it and returned in `recruiterCommissions`. A recruiter commission itself can't be approved before the commission it is a share of (`409 source_not_approved`).\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"parameters":[{"schema":{"type":"string","description":"The id."},"required":true,"description":"The id.","name":"id","in":"path"}],"responses":{"200":{"description":"Approved.","content":{"application/json":{"schema":{"type":"object","properties":{"commission":{"$ref":"#/components/schemas/Commission"},"recruiterCommissions":{"type":"array","items":{"$ref":"#/components/schemas/Commission"}}},"required":["commission"]}}}},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`holdback_active`, `not_approvable`, `source_not_approved`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/commissions/{id}/reverse":{"post":{"operationId":"reverseCommission","tags":["Commissions"],"summary":"Reverse an accrued or approved commission","description":"Fires `commission.reversed` with `reason: \"manual\"`.\n\nA recruiter's share of it (two-tier, `sourceCommissionId`) goes with it, in `recruiterCommissions`: an accrued or approved one is `reversed`; a paid one stays paid and gets a refund clawback for what the recruiter holds, netted off their next manual or direct payout (`clawbacks`); one a payout is moving right now is left as it is (`heldInTransfer`).\n\nTo finish a held share, call this again on the same (already reversed) commission once the payout settles: it answers `200` with what it did to the share (a clawback, if it was paid), and `409 not_reversible` once there is nothing left to do.\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"parameters":[{"schema":{"type":"string","description":"The id."},"required":true,"description":"The id.","name":"id","in":"path"}],"responses":{"200":{"description":"Reversed.","content":{"application/json":{"schema":{"type":"object","properties":{"commission":{"$ref":"#/components/schemas/Commission"},"recruiterCommissions":{"type":"object","properties":{"reversed":{"type":"array","items":{"$ref":"#/components/schemas/Commission"}},"clawbacks":{"type":"array","items":{"type":"object","properties":{"commissionId":{"type":"string"},"partnerId":{"type":"string"},"amount":{"$ref":"#/components/schemas/Decimal"},"currency":{"type":"string"}},"required":["commissionId","partnerId","amount","currency"]}},"heldInTransfer":{"type":"array","items":{"type":"string"}}},"required":["reversed","clawbacks","heldInTransfer"]}},"required":["commission"]}}}},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`commission_in_transfer`, `not_reversible`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/bonuses":{"post":{"operationId":"awardBonus","tags":["Bonuses"],"summary":"Pay a partner a bonus","description":"A reward that isn't tied to a sale: a launch bonus, a contest prize, a thank-you. It is written as a bonus and one commission (`source: \"bonus\"` in commission lists, `bonusId` set, no `attributionId`), so it is approved, paid and reversed like any commission, but it never counts as a sale, a lead or toward a tier.\n\n- `amount` is in major units with at most two decimals (`400 invalid_amount` otherwise).\n- Approved at once and paid in the next payout run. `hold: true` holds it for the program's holdback first (`programId`), or the partner's own.\n- `externalRef` makes a retry safe: the same value returns the original (`200`, `replayed: true`). The same `externalRef` with a different partner, program, amount or currency is `409 external_ref_conflict`.\n- Fires `bonus.awarded`, then `bonus.approved` unless held. Never `commission.*`.\n- On OpenPartner Cloud's commission-floor fee terms a bonus is billed at the commission rate, like a fixed commission with no sale amount.\n\nCalled with the brand's OpenPartner Network federation key, the bonus is recorded as `source: \"network\"`; every other caller pays `manual` bonuses. On OpenPartner Cloud the brand needs an active plan (`402 plan_required`) and an approved account (`403 brand_pending_review`).\n\n**Access:** admin key, or a scoped key with `bonuses:write`.","security":[{"adminKey":[]},{"scopedKey":["bonuses:write"]}],"x-openpartner-access":{"kind":"admin","scopes":["bonuses:write"],"partner":false},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AwardBonusRequest"}}}},"responses":{"200":{"description":"Replayed (`replayed: true`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BonusAwarded"}}}},"201":{"description":"Awarded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BonusAwarded"}}}},"400":{"description":"`invalid_body`, `tenant_slug_missing`, `invalid_amount`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"`plan_required`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`, `brand_pending_review`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`partner_not_found`, `program_not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`partner_revoked`, `external_ref_conflict`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"operationId":"listBonuses","tags":["Bonuses"],"summary":"List bonuses","description":"Newest first, keyset-paginated: pass `nextCursor` back as `cursor`. Each carries its commission's id and status.\n\n**Access:** admin key, or a scoped key with `commissions:read` or `bonuses:write`.","security":[{"adminKey":[]},{"scopedKey":["commissions:read"]},{"scopedKey":["bonuses:write"]}],"x-openpartner-access":{"kind":"admin","scopes":["commissions:read","bonuses:write"],"partner":false},"parameters":[{"schema":{"type":"string","minLength":1},"required":false,"name":"partnerId","in":"query"},{"schema":{"type":"string","minLength":1},"required":false,"name":"programId","in":"query"},{"schema":{"type":"string","minLength":1},"required":false,"name":"bountyId","in":"query"},{"schema":{"type":"string","enum":["manual","performance_bounty","network"]},"required":false,"name":"source","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":500,"default":100},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string"},"required":false,"name":"cursor","in":"query"}],"responses":{"200":{"description":"One page.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BonusPage"}}}},"400":{"description":"`invalid_query`, `tenant_slug_missing`, `invalid_cursor`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/bonuses/{id}":{"get":{"operationId":"getBonus","tags":["Bonuses"],"summary":"Get a bonus","description":"**Access:** admin key, or a scoped key with `commissions:read` or `bonuses:write`.","security":[{"adminKey":[]},{"scopedKey":["commissions:read"]},{"scopedKey":["bonuses:write"]}],"x-openpartner-access":{"kind":"admin","scopes":["commissions:read","bonuses:write"],"partner":false},"parameters":[{"schema":{"type":"string","description":"The id."},"required":true,"description":"The id.","name":"id","in":"path"}],"responses":{"200":{"description":"The bonus.","content":{"application/json":{"schema":{"type":"object","properties":{"bonus":{"$ref":"#/components/schemas/BonusListItem"}},"required":["bonus"]}}}},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/payouts/run":{"post":{"operationId":"runPayouts","tags":["Payouts"],"summary":"Run a payout batch now","description":"Groups approved commissions per partner and currency. Stripe Connect partners are transferred. Everyone else (every headless partner) gets a `manual` payout: the Payout is `pending` and its commissions are marked `paid` right away, before any money moves. You send the money yourself, then `POST /payouts/{id}/confirm`. Totals under the payout threshold carry over. The scheduled jobs run this on the tenant's payout cadence (weekly by default; set it to Manual to run it only yourself).\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"responses":{"200":{"description":"What happened to each payout.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PayoutRunResult"}}}},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/payouts":{"get":{"operationId":"listPayouts","tags":["Payouts"],"summary":"List all payouts","description":"Every partner's payouts with the partner's name, newest first, keyset-paginated: pass `nextCursor` back as `cursor` (`limit`, default 50, at most 200). Filter with `status`. A `manual` payout stays `pending` until you confirm you sent it.\n\n**Access:** admin key, or a scoped key with `payouts:read`.","security":[{"adminKey":[]},{"scopedKey":["payouts:read"]}],"x-openpartner-access":{"kind":"admin","scopes":["payouts:read"],"partner":false},"parameters":[{"schema":{"type":"string","enum":["pending","paid","failed","partially_reversed","reversed"]},"required":false,"name":"status","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","maxLength":200},"required":false,"name":"cursor","in":"query"}],"responses":{"200":{"description":"One page.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PayoutPage"}}}},"400":{"description":"`invalid_query`, `tenant_slug_missing`, `invalid_cursor`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/payouts/holds":{"get":{"operationId":"listPayoutHolds","tags":["Payouts"],"summary":"List partners whose payouts are on hold","description":"OpenPartner Cloud only. Partners with approved commissions who are on a compliance hold: their declared country isn't served, or their sanctions screening has a possible match that's waiting for review, a confirmed match, or no answer yet. Payouts through Stripe (`effect: \"held\"`) aren't made; the commissions stay approved and are paid on the first run after the hold lifts. A partner you pay yourself (`effect: \"warning\"`) still gets a `manual` payout, so check before you send it. OpenPartner reviews every match; you don't need to do anything.\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"responses":{"200":{"description":"Held partners.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PayoutHolds"}}}},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/payouts/{id}/confirm":{"post":{"operationId":"confirmManualPayout","tags":["Payouts"],"summary":"Mark a manual payout as sent","description":"Only a `pending` manual payout. On OpenPartner Cloud a payout on a do-not-pay hold (the partner or their payout account in a country OpenPartner doesn’t serve, or a confirmed sanctions-list match) is refused with `409 not_confirmable` and a `reason`.\n\nThe body is optional. For a payout you sent one at a time (a bank transfer, UPI, a cheque), `reference` records what identifies the payment: the bank's UTR, the UPI transaction ID, the cheque number. Up to 100 characters on one line, trimmed. It is kept on the payout as `metadata.externalReference` and printed on its statement as the `Payment reference` (`GET /payouts/{id}/statement`), unless the payout is in a PayPal or Wise file, whose own reference is printed instead. It can't be added or changed after the payout is confirmed.\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"parameters":[{"schema":{"type":"string","description":"The id."},"required":true,"description":"The id.","name":"id","in":"path"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfirmManualPayoutRequest"}}}},"responses":{"200":{"description":"Confirmed.","content":{"application/json":{"schema":{"type":"object","properties":{"payout":{"$ref":"#/components/schemas/Payout"}},"required":["payout"]}}}},"400":{"description":"`invalid_body`, `tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`payout_not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`not_confirmable`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/partners/{id}/payouts":{"get":{"operationId":"listPartnerPayouts","tags":["Payouts"],"summary":"List a partner's payouts","description":"The 200 most recent, newest first. A payout PayPal can’t send in its own currency (INR) that Pay with PayPal sent in USD carries `sent`: the USD amount and the ECB rate and date it was converted at. Its `amount` and `currency` stay its own.\n\n**Access:** admin key, a scoped key with `payouts:read`, or the partner's own key (`:id` must be that partner).","security":[{"adminKey":[]},{"partnerKey":[]},{"scopedKey":["payouts:read"]}],"x-openpartner-access":{"kind":"partnerOrAdmin","scopes":["payouts:read"],"partner":true},"parameters":[{"schema":{"type":"string","description":"The id."},"required":true,"description":"The id.","name":"id","in":"path"}],"responses":{"200":{"description":"Payouts.","content":{"application/json":{"schema":{"type":"object","properties":{"payouts":{"type":"array","items":{"$ref":"#/components/schemas/PayoutWithSent"}}},"required":["payouts"]}}}},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/payouts/{id}/statement":{"get":{"operationId":"getPayoutStatement","tags":["Payouts"],"summary":"Get a payout's statement","description":"Remittance advice for one payout: who paid whom, how, and for what. A printable HTML page by default (print it, or save it as a PDF from the browser's print dialog); `?format=json` returns the same data.\n\nIt lists the payout's commissions (date, program, the event it was earned on, amount), its bonuses, and the refund clawbacks deducted from it, with totals per currency and the amount paid. It names your business (the legal name and address from `POST /config/payout-statements`, else the program name; on OpenPartner Cloud, the GSTIN on your billing profile), the partner, and where the payout went, **masked**: a full account number, email or UPI ID is never shown. A pending or failed payout says so.\n\nIt is built from the payout's records each time, so it always agrees with them. It is not a tax invoice.\n\n**Access:** an admin key, a key holding `payouts:read`, or a partner (session or key) for one of their own payouts. Another partner's payout answers `404 payout_not_found`, like one that doesn't exist.","security":[{"adminKey":[]},{"partnerKey":[]},{"scopedKey":["payouts:read"]}],"x-openpartner-access":{"kind":"handler","scopes":["payouts:read"],"partner":true},"parameters":[{"schema":{"type":"string","description":"The id."},"required":true,"description":"The id.","name":"id","in":"path"},{"schema":{"type":"string","enum":["html","json"],"default":"html"},"required":false,"name":"format","in":"query"}],"responses":{"200":{"description":"The statement.","content":{"text/html":{"schema":{"type":"string"}},"application/json":{"schema":{"type":"object","properties":{"statement":{"$ref":"#/components/schemas/PayoutStatement"}},"required":["statement"]}}}},"400":{"description":"`invalid_query`, `tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`payout_not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/payouts/confirm":{"post":{"operationId":"confirmManualPayouts","tags":["Payouts"],"summary":"Mark many manual payouts as sent","description":"Each id is confirmed as `POST /payouts/{id}/confirm` would, or reported in `notConfirmable` (with a `reason` for a do-not-pay hold); one that can’t be confirmed doesn’t stop the rest. Up to 5,000 ids. To confirm a PayPal or Wise file, use `POST /payouts/exports/{exportId}/confirm`, which confirms exactly the payouts in it. This takes no payment references: to record one for a payout you sent one at a time, confirm it with `POST /payouts/{id}/confirm`. No webhooks: `payout.created` and `commission.paid` fired when the run made the payouts.\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"payoutIds":{"type":"array","items":{"type":"string","minLength":1,"maxLength":64},"minItems":1,"maxItems":5000}},"required":["payoutIds"]}}}},"responses":{"200":{"description":"What was confirmed, and what wasn’t.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkConfirmResult"}}}},"400":{"description":"`invalid_body`, `tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/payouts/runs":{"get":{"operationId":"listManualPayoutRuns","tags":["Payouts"],"summary":"List payout runs with manual payouts to send","description":"Runs that still have `pending` manual payouts, newest first, a page at a time (`limit`, default 50, at most 200; `nextCursor`).\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"parameters":[{"schema":{"type":"string","maxLength":200},"required":false,"name":"cursor","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"One page of runs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ManualPayoutRuns"}}}},"400":{"description":"`invalid_query`, `tenant_slug_missing`, `invalid_cursor`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/payouts/runs/{runId}":{"get":{"operationId":"getManualPayoutRun","tags":["Payouts"],"summary":"Summarise a run’s manual payouts to send","description":"What a new PayPal or Wise file would take (`ready`), the files already made that still hold payouts to confirm (`exports`), and why the rest are in no file (`excluded`). List the payouts themselves with `GET /payouts/runs/{runId}/payouts`. See docs/manual-payout-files.md.\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"parameters":[{"schema":{"type":"string","description":"The runId."},"required":true,"description":"The runId.","name":"runId","in":"path"}],"responses":{"200":{"description":"The run.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ManualPayoutRun"}}}},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`run_not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/payouts/runs/{runId}/payouts":{"get":{"operationId":"listManualPayoutRunPayouts","tags":["Payouts"],"summary":"List a run’s manual payouts to send","description":"Pending manual payouts in id order, a page at a time (`limit`, default 100, at most 500; `nextCursor`): each one’s file, or why it’s in none, with compliance warnings and masked payout details. Confirmed payouts leave the list.\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"parameters":[{"schema":{"type":"string","description":"The runId."},"required":true,"description":"The runId.","name":"runId","in":"path"},{"schema":{"type":"string","maxLength":200},"required":false,"name":"cursor","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":500,"default":100},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"One page of payouts.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ManualRunPayoutPage"}}}},"400":{"description":"`invalid_query`, `tenant_slug_missing`, `invalid_cursor`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`run_not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/payouts/runs/{runId}/exports":{"post":{"operationId":"createManualPayoutFile","tags":["Payouts"],"summary":"Make a batch file for your PayPal or Wise account","description":"The next file of a run for one provider and currency: the first payouts no file holds yet that belong in it, in id order, up to what one upload takes. The money goes from you to the partner; it never passes through OpenPartner.\n\nThe file's payouts are fixed here: each is stamped with the `exportId`, held out of every other file, and confirmed together with `POST /payouts/exports/{exportId}/confirm` once your batch has gone out. If you never upload it, `POST /payouts/exports/{exportId}/discard` releases them.\n\n- **`paypal`**: PayPal Payouts (Send Money → Make a Mass Payment). No header row; ten columns: recipient, amount, currency, Customer ID (the payout id), note, `PAYPAL`, three empty Venmo columns, purpose `SERVICES`. `currency` is the payout currency. EUR and BRL amounts use a decimal comma; HUF, JPY and TWD are whole units. Up to 5,000 rows.\n- **`wise`**: Wise batch payments, \"Send to bank accounts\". A header row; `currency` is the source currency you pay in, and each row's `targetCurrency` is the partner's account currency. Up to 1,000 rows.\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"parameters":[{"schema":{"type":"string","description":"The runId."},"required":true,"description":"The runId.","name":"runId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"provider":{"type":"string","enum":["paypal","wise"]},"currency":{"type":"string","pattern":"^[A-Za-z]{3}$"}},"required":["provider","currency"]}}}},"responses":{"201":{"description":"The file and the payouts it holds.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ManualExportFile"}}}},"400":{"description":"`invalid_body`, `tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`nothing_to_export`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/payouts/exports/{exportId}.csv":{"get":{"operationId":"downloadManualPayoutFile","tags":["Payouts"],"summary":"Download a batch file again","description":"The payouts the file still holds that aren’t confirmed, with the partners’ current details. A payout whose partner has since switched to details that don’t fit the file, or that is now on a do-not-pay hold, is left out (`changedCount` and `doNotPayCount` on `GET /payouts/runs/{runId}`). If you uploaded the first copy, don’t upload this one: confirm the file instead.\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"parameters":[{"schema":{"type":"string","description":"The exportId."},"required":true,"description":"The exportId.","name":"exportId","in":"path"}],"responses":{"200":{"description":"The file (UTF-8 CSV).","content":{"text/csv":{"schema":{"type":"string"}}}},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`file_not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/payouts/exports/{exportId}/confirm":{"post":{"operationId":"confirmManualPayoutFile","tags":["Payouts"],"summary":"Mark a batch file’s payouts as sent","description":"Confirms exactly the payouts the file holds that aren’t confirmed yet, whatever a fresh look at the run would put in a file now. A payout now on a do-not-pay hold is refused and reported.\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"parameters":[{"schema":{"type":"string","description":"The exportId."},"required":true,"description":"The exportId.","name":"exportId","in":"path"}],"responses":{"200":{"description":"What was confirmed, and what wasn’t.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkConfirmResult"}}}},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`export_not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/payouts/exports/{exportId}/discard":{"post":{"operationId":"discardManualPayoutFile","tags":["Payouts"],"summary":"Discard a batch file you never uploaded","description":"Releases the file’s payouts that aren’t confirmed, so a new file can take them. Only for a file that never went out: once you’ve uploaded it, confirm it instead.\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"parameters":[{"schema":{"type":"string","description":"The exportId."},"required":true,"description":"The exportId.","name":"exportId","in":"path"}],"responses":{"200":{"description":"Released.","content":{"application/json":{"schema":{"type":"object","properties":{"released":{"type":"integer","description":"Payouts released."}},"required":["released"]}}}},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`export_not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/config/paypal":{"get":{"operationId":"listPaypalConnections","tags":["Payouts"],"summary":"Your connected PayPal apps","description":"The PayPal REST apps Pay with PayPal sends with, live and/or sandbox, and which one is active. The secret is never returned. See docs/paypal-payouts.md.\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"responses":{"200":{"description":"Connections.","content":{"application/json":{"schema":{"type":"object","properties":{"connections":{"type":"array","items":{"$ref":"#/components/schemas/PaypalConnection"}}},"required":["connections"]}}}},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"connectPaypal","tags":["Payouts"],"summary":"Connect your PayPal app","description":"Saves your PayPal REST app's client id and secret for one environment, after a token call proves them. The secret is sealed at rest, never returned, never logged and never exported. Pay with PayPal then calls PayPal's Payouts API as your app: money goes from your PayPal balance to your partners and never passes through OpenPartner.\n\nThe first environment you connect becomes the one Pay with PayPal uses; switch with `POST /config/paypal/{environment}/activate`. Connecting again replaces that environment's credentials. A different app's are refused (`409 open_batches`) while batches sent with the current ones are in flight; a new secret for the same app takes them over.\n\nOpenPartner also registers a webhook on the app for the Payouts events, when the deployment has a public https address.\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"environment":{"type":"string","enum":["live","sandbox"]},"clientId":{"type":"string","pattern":"^[A-Za-z0-9_-]{8,128}$"},"clientSecret":{"type":"string","pattern":"^[A-Za-z0-9_-]{8,128}$"}},"required":["environment","clientId","clientSecret"]}}}},"responses":{"201":{"description":"Connected.","content":{"application/json":{"schema":{"type":"object","properties":{"connection":{"$ref":"#/components/schemas/PaypalConnection"}},"required":["connection"]}}}},"400":{"description":"`invalid_body`, `tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`open_batches`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"`credentials_refused`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"`paypal_unreachable`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/config/paypal/{environment}/verify":{"post":{"operationId":"verifyPaypalConnection","tags":["Payouts"],"summary":"Check your PayPal credentials again","description":"A fresh token call with the saved credentials. Also refreshes whether PayPal has enabled Payouts on the app (`payoutsEnabled`).\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"parameters":[{"schema":{"type":"string","enum":["live","sandbox"]},"required":true,"name":"environment","in":"path"}],"responses":{"200":{"description":"Verified.","content":{"application/json":{"schema":{"type":"object","properties":{"connection":{"$ref":"#/components/schemas/PaypalConnection"}},"required":["connection"]}}}},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_connected`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"`credentials_refused`, `credentials_unreadable`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"`paypal_unreachable`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/config/paypal/{environment}/activate":{"post":{"operationId":"activatePaypalEnvironment","tags":["Payouts"],"summary":"Choose live or sandbox for Pay with PayPal","description":"Pay with PayPal sends with this environment from now on. Batches already sent keep the app they were sent with.\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"parameters":[{"schema":{"type":"string","enum":["live","sandbox"]},"required":true,"name":"environment","in":"path"}],"responses":{"200":{"description":"Active.","content":{"application/json":{"schema":{"type":"object","properties":{"connection":{"$ref":"#/components/schemas/PaypalConnection"}},"required":["connection"]}}}},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_connected`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/config/paypal/{environment}":{"delete":{"operationId":"disconnectPaypal","tags":["Payouts"],"summary":"Disconnect a PayPal app","description":"Removes OpenPartner’s webhook from the app and wipes the saved secret and token. Refused while batches sent with it are in flight (`409 open_batches`): their status can only be read with it. Never makes the other environment active.\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"parameters":[{"schema":{"type":"string","enum":["live","sandbox"]},"required":true,"name":"environment","in":"path"}],"responses":{"204":{"description":"Disconnected."},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_connected`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`open_batches`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/payouts/runs/{runId}/paypal-batches":{"post":{"operationId":"sendRunWithPaypal","tags":["Payouts"],"summary":"Pay a run’s PayPal partners through your PayPal app","description":"Sends the next PayPal Payouts batch of a run, in one currency, with your active PayPal app: the same payouts a PayPal file would take (partners whose payout details are a PayPal email, a currency PayPal sends, under PayPal's per-payment limit, nothing on a do-not-pay hold), up to 5,000. The money goes from your PayPal balance to the partners; it never passes through OpenPartner.\n\nThe payouts are stamped with the batch id (`exportId`, `via: \"api\"`) before anything is sent, so they are in no other file or batch. The batch id is also PayPal's `sender_batch_id` and `PayPal-Request-Id`: a re-send after a timeout or crash reaches the same batch, and PayPal never pays one twice.\n\nPayPal pays asynchronously. Follow the batch with `GET /payouts/paypal-batches/{batchId}`: paid items confirm their payouts; failed, returned or refunded ones go back to pending (and your admins are emailed); unclaimed ones wait for the partner to claim them.\n\n**INR.** PayPal can't send INR (it isn't a PayPal API currency): an INR payout is `currency_not_supported` unless you turn on `POST /config/paypal-payouts` `{\"sendUnsupportedAs\": \"USD\"}`. Then quote first (`POST /payouts/runs/{runId}/paypal-batches/quote`) and send with `{\"currency\": \"INR\", \"sendAs\": \"USD\", \"quoteId\": \"…\"}` within 15 minutes: the batch sends exactly the quoted payouts at exactly the quoted ECB rate. A quote that expired is `409 quote_expired`; one whose payouts changed since (another batch took one, a partner changed their PayPal email, PayPal gave one back) or whose PayPal app is no longer the active one is `409 quote_changed`; quote again. The payouts stay in INR; each one's stamp records what was sent (`conversion`).\n\nWith your **sandbox** app active, the batch is a rehearsal: PayPal's sandbox moves test money, so no payout is marked paid, and once PayPal settles every item the payouts go back to pending (`rehearsal` on the batch says what happened).\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"parameters":[{"schema":{"type":"string","description":"The runId."},"required":true,"description":"The runId.","name":"runId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"currency":{"type":"string","pattern":"^[A-Za-z]{3}$"},"sendAs":{"type":"string","enum":["USD"],"description":"Send balances PayPal can’t send in `currency` (INR) in this currency instead, at the rate of `quoteId`. Needs the `paypal_payouts` setting."},"quoteId":{"type":"string","minLength":1,"maxLength":2048,"description":"With `sendAs`: from `POST /payouts/runs/{runId}/paypal-batches/quote`, within 15 minutes. The batch is sent at exactly its rate, and only if the payouts are still exactly the quoted ones."}},"required":["currency"]}}}},"responses":{"201":{"description":"The batch, as it stands after the first send.","content":{"application/json":{"schema":{"type":"object","properties":{"batch":{"$ref":"#/components/schemas/PaypalBatch"}},"required":["batch"]}}}},"400":{"description":"`invalid_body`, `tenant_slug_missing`, `invalid_quote`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`nothing_to_send`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`paypal_not_connected`, `conversion_not_enabled`, `quote_expired`, `quote_changed`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"`currency_not_supported`, `conversion_not_needed`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"operationId":"listRunPaypalBatches","tags":["Payouts"],"summary":"List a run’s PayPal Payouts batches","description":"Newest first, up to 50.\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"parameters":[{"schema":{"type":"string","description":"The runId."},"required":true,"description":"The runId.","name":"runId","in":"path"}],"responses":{"200":{"description":"Batches.","content":{"application/json":{"schema":{"type":"object","properties":{"batches":{"type":"array","items":{"$ref":"#/components/schemas/PaypalBatch"}}},"required":["batches"]}}}},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/payouts/runs/{runId}/paypal-batches/quote":{"post":{"operationId":"quotePaypalConversion","tags":["Payouts"],"summary":"Quote sending INR balances through PayPal in USD","description":"For balances PayPal can't send in their own currency (INR), with `POST /config/paypal-payouts` `{\"sendUnsupportedAs\": \"USD\"}` on. Quotes the next batch the send would make: the European Central Bank reference rate (crossed through EUR, published once a business day), its date, each payout's USD amount (its INR amount × rate, rounded half up to the cent on its own), the INR and USD totals, and a signed `quoteId` valid for 15 minutes.\n\nSend with `POST /payouts/runs/{runId}/paypal-batches` `{\"currency\": \"INR\", \"sendAs\": \"USD\", \"quoteId\": \"…\"}`. Nothing is held: the send checks the payouts are still exactly these. A payout over PayPal's per-payment limit in USD, or under a cent, is listed in `leftOut` instead; when that's every payout, the `404 nothing_to_send` carries the `leftOut` list.\n\nThe rate is the ECB mid-market reference rate; you send that USD amount from your PayPal balance. PayPal then converts it to INR for the partner's Indian bank at PayPal's own rate, which is between the partner and PayPal. If the ECB rates can't be read, nothing is quoted (`503 fx_unavailable`): no rate is ever guessed.\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"parameters":[{"schema":{"type":"string","description":"The runId."},"required":true,"description":"The runId.","name":"runId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"currency":{"type":"string","pattern":"^[A-Za-z]{3}$"},"sendAs":{"type":"string","enum":["USD"]}},"required":["currency","sendAs"]}}}},"responses":{"200":{"description":"The quote.","content":{"application/json":{"schema":{"type":"object","properties":{"quote":{"$ref":"#/components/schemas/PaypalConversionQuote"}},"required":["quote"]}}}},"400":{"description":"`invalid_body`, `tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`nothing_to_send`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`paypal_not_connected`, `conversion_not_enabled`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"`currency_not_supported`, `conversion_not_needed`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"`fx_unavailable`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/config/paypal-payouts":{"get":{"operationId":"getPaypalPayoutSettings","tags":["Payouts"],"summary":"Your Pay with PayPal settings","description":"Whether balances PayPal can’t send in their own currency (INR) are sent in USD instead. Off by default. Exported with your settings (the `paypal_payouts` Config key).\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"responses":{"200":{"description":"Settings.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaypalPayoutSettings"}}}},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"updatePaypalPayoutSettings","tags":["Payouts"],"summary":"Change your Pay with PayPal settings","description":"`{\"sendUnsupportedAs\": \"USD\"}`: INR payouts to partners with a PayPal email become ready for Pay with PayPal, sent in USD at an ECB rate you are quoted and see before each batch. `null` turns it off; batches already sent are unaffected. Omitted fields keep their value.\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"sendUnsupportedAs":{"type":["string","null"],"enum":["USD",null],"description":"Pay balances PayPal can’t send (INR) through Pay with PayPal in this currency, at the ECB reference rate quoted before each batch. `null` turns it off."}}}}}},"responses":{"200":{"description":"Saved.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaypalPayoutSettings"}}}},"400":{"description":"`invalid_body`, `tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/payouts/paypal-batches/{batchId}":{"get":{"operationId":"getPaypalBatch","tags":["Payouts"],"summary":"Get a PayPal Payouts batch","description":"Where the batch stands, as of the last read from PayPal (`lastSyncedAt`).\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"parameters":[{"schema":{"type":"string","description":"The batchId."},"required":true,"description":"The batchId.","name":"batchId","in":"path"}],"responses":{"200":{"description":"The batch.","content":{"application/json":{"schema":{"type":"object","properties":{"batch":{"$ref":"#/components/schemas/PaypalBatch"}},"required":["batch"]}}}},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`batch_not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/payouts/paypal-batches/{batchId}/sync":{"post":{"operationId":"syncPaypalBatch","tags":["Payouts"],"summary":"Check a PayPal Payouts batch now","description":"Reads the batch from PayPal now instead of at the next poll, and applies what it says. A batch whose send had no answer yet is re-sent (it reaches the same batch).\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"parameters":[{"schema":{"type":"string","description":"The batchId."},"required":true,"description":"The batchId.","name":"batchId","in":"path"}],"responses":{"200":{"description":"The batch.","content":{"application/json":{"schema":{"type":"object","properties":{"batch":{"$ref":"#/components/schemas/PaypalBatch"}},"required":["batch"]}}}},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`batch_not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/payouts/paypal-batches/{batchId}/resolve":{"post":{"operationId":"resolvePaypalBatch","tags":["Payouts"],"summary":"Settle a PayPal batch OpenPartner can’t","description":"For a batch in `reconcile_required` (OpenPartner can't tell whether it went out), after you've looked in your PayPal account:\n\n- `{\"action\": \"link\", \"payoutBatchId\": \"…\"}`: PayPal shows the batch. It is read back and adopted only if its sender_batch_id is this batch's id.\n- `{\"action\": \"retry\"}`: send the same request again. Only within 29 days of the first send, while PayPal still refuses a duplicate.\n- `{\"action\": \"release\"}`: no such batch went out. The payouts go back to pending. Also abandons a batch still in `intent`, and a **sandbox** batch in any open state (it moves test money only).\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"parameters":[{"schema":{"type":"string","description":"The batchId."},"required":true,"description":"The batchId.","name":"batchId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"type":"object","properties":{"action":{"type":"string","enum":["retry"]}},"required":["action"]},{"type":"object","properties":{"action":{"type":"string","enum":["release"]}},"required":["action"]},{"type":"object","properties":{"action":{"type":"string","enum":["link"]},"payoutBatchId":{"type":"string","pattern":"^[A-Za-z0-9]{1,64}$"}},"required":["action","payoutBatchId"]}]}}}},"responses":{"200":{"description":"The batch.","content":{"application/json":{"schema":{"type":"object","properties":{"batch":{"$ref":"#/components/schemas/PaypalBatch"}},"required":["batch"]}}}},"400":{"description":"`invalid_body`, `tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`batch_not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`not_resolvable`, `not_retryable`, `not_connected`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"`batch_not_ours`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"`paypal_unreachable`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/payouts/{id}/paypal-cancel":{"post":{"operationId":"cancelUnclaimedPaypalPayout","tags":["Payouts"],"summary":"Cancel an unclaimed PayPal payout","description":"PayPal returns an unclaimed payment after 30 days. This cancels it now: PayPal returns the money to your balance and the payout goes back to pending. If the partner has just claimed it, nothing is cancelled and the payout follows what PayPal says.\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"parameters":[{"schema":{"type":"string","description":"The id."},"required":true,"description":"The id.","name":"id","in":"path"}],"responses":{"200":{"description":"What PayPal said.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"released":{"type":"boolean"},"status":{"type":"string","description":"The item’s transaction_status now."}},"required":["ok","released","status"]}}}},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`payout_not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`not_unclaimed`, `not_connected`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"`paypal_unreachable`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/partners/{id}/payout-profile":{"get":{"operationId":"getPartnerPayoutProfile","tags":["Payouts"],"summary":"Get a partner's payout details","description":"Where the partner wants manual payouts sent, in full, how they are paid today (`paidVia`), and the brand's payout rail (`rail`). Sensitive: show it only to the partner and the people who pay them.\n\n**Access:** admin key, a scoped key with `payout_profiles:read` or `payout_profiles:write`, or the partner's own key (`:id` must be that partner).","security":[{"adminKey":[]},{"partnerKey":[]},{"scopedKey":["payout_profiles:read"]},{"scopedKey":["payout_profiles:write"]}],"x-openpartner-access":{"kind":"partnerOrAdmin","scopes":["payout_profiles:read","payout_profiles:write"],"partner":true},"parameters":[{"schema":{"type":"string","description":"The id."},"required":true,"description":"The id.","name":"id","in":"path"}],"responses":{"200":{"description":"The payout details, or null.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerPayoutProfileResponse"}}}},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`partner_not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"put":{"operationId":"setPartnerPayoutProfile","tags":["Payouts"],"summary":"Set a partner's payout details","description":"Replaces the partner's payout details, for the manual rail's batch files. Stored encrypted. The partner is emailed a masked notice of every change (unless they have no portal access), so a change they didn't make doesn't go unnoticed.\n\n**Access:** admin key, a scoped key with `payout_profiles:write`, or the partner's own key (`:id` must be that partner).","security":[{"adminKey":[]},{"partnerKey":[]},{"scopedKey":["payout_profiles:write"]}],"x-openpartner-access":{"kind":"partnerOrAdmin","scopes":["payout_profiles:write"],"partner":true},"parameters":[{"schema":{"type":"string","description":"The id."},"required":true,"description":"The id.","name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PayoutDetails"}}}},"responses":{"200":{"description":"Saved.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerPayoutProfileResponse"}}}},"400":{"description":"`invalid_body`, `tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`partner_not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"operationId":"deletePartnerPayoutProfile","tags":["Payouts"],"summary":"Remove a partner's payout details","description":"**Access:** admin key, a scoped key with `payout_profiles:write`, or the partner's own key (`:id` must be that partner).","security":[{"adminKey":[]},{"partnerKey":[]},{"scopedKey":["payout_profiles:write"]}],"x-openpartner-access":{"kind":"partnerOrAdmin","scopes":["payout_profiles:write"],"partner":true},"parameters":[{"schema":{"type":"string","description":"The id."},"required":true,"description":"The id.","name":"id","in":"path"}],"responses":{"204":{"description":"Removed."},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`partner_not_found`, `payout_profile_not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/partners/{id}/dashboard":{"get":{"operationId":"getPartnerDashboard","tags":["Partner reporting"],"summary":"Headline stats for a partner","description":"Clicks, attributed events and revenue, and commission totals by status, since `since`. Amounts in different currencies are never added together: `attributedRevenue` and `commissionByStatus` are in `currency` (the brand's default currency), and the `…ByCurrency` fields hold every currency.\n\n**Access:** admin key, a scoped key with `partners:read`, or the partner's own key (`:id` must be that partner).","security":[{"adminKey":[]},{"partnerKey":[]},{"scopedKey":["partners:read"]}],"x-openpartner-access":{"kind":"partnerOrAdmin","scopes":["partners:read"],"partner":true},"parameters":[{"schema":{"type":"string","description":"The id."},"required":true,"description":"The id.","name":"id","in":"path"},{"schema":{"type":"string","format":"date-time","description":"ISO-8601 start of the window. Default: 30 days ago. Not validated by the route: send a valid timestamp.","pattern":"Z$"},"required":false,"description":"ISO-8601 start of the window. Default: 30 days ago. Not validated by the route: send a valid timestamp.","name":"since","in":"query"},{"schema":{"type":"string","enum":["0","1","true","false"],"description":"Add a per-link breakdown."},"required":false,"description":"Add a per-link breakdown.","name":"includeLinks","in":"query"}],"responses":{"200":{"description":"Stats.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerDashboard"}}}},"400":{"description":"`invalid_query`, `tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/partners/{id}/timeseries":{"get":{"operationId":"getPartnerTimeseries","tags":["Partner reporting"],"summary":"Daily or weekly series for charts","description":"Dense buckets (zero-filled). Defaults to the last 30 days, bucketed by day, or by week past 90 days. Event counts are per attribution: an event credited through two of this partner's clicks (a multi-touch model) counts twice. `leads` counts `signup` and `trial_started`; `sales` counts any event with a value above 0.\n\n**Access:** admin key, a scoped key with `partners:read`, or the partner's own key (`:id` must be that partner).","security":[{"adminKey":[]},{"partnerKey":[]},{"scopedKey":["partners:read"]}],"x-openpartner-access":{"kind":"partnerOrAdmin","scopes":["partners:read"],"partner":true},"parameters":[{"schema":{"type":"string","description":"The id."},"required":true,"description":"The id.","name":"id","in":"path"},{"schema":{"type":"string","format":"date-time","pattern":"Z$"},"required":false,"name":"since","in":"query"},{"schema":{"type":"string","format":"date-time","pattern":"Z$"},"required":false,"name":"until","in":"query"},{"schema":{"type":"string","minLength":1},"required":false,"name":"programId","in":"query"},{"schema":{"type":"string","enum":["day","week"]},"required":false,"name":"bucket","in":"query"}],"responses":{"200":{"description":"Series.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerTimeseries"}}}},"400":{"description":"`invalid_query`, `tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/partners/{id}/events":{"get":{"operationId":"listPartnerEvents","tags":["Partner reporting"],"summary":"Attributed events for a partner","description":"Newest first, offset-paginated (default 50, max 200), with the click each event was credited through. A partner calling gets `userId: null` and the customer's `referralId` instead, and only events of the types the brand shows as referral stages (`GET /config/referrals`); admins and scoped keys get every type and the userId.\n\n**Access:** admin key, a scoped key with `partners:read`, or the partner's own key (`:id` must be that partner).","security":[{"adminKey":[]},{"partnerKey":[]},{"scopedKey":["partners:read"]}],"x-openpartner-access":{"kind":"partnerOrAdmin","scopes":["partners:read"],"partner":true},"parameters":[{"schema":{"type":"string","description":"The id."},"required":true,"description":"The id.","name":"id","in":"path"},{"schema":{"type":"string","minLength":1},"required":false,"name":"programId","in":"query"},{"schema":{"type":"string","minLength":1,"maxLength":80},"required":false,"name":"type","in":"query"},{"schema":{"type":"string","format":"date-time","pattern":"Z$"},"required":false,"name":"since","in":"query"},{"schema":{"type":"string","format":"date-time","pattern":"Z$"},"required":false,"name":"until","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200},"required":false,"name":"limit","in":"query"},{"schema":{"type":["integer","null"],"minimum":0},"required":false,"name":"offset","in":"query"}],"responses":{"200":{"description":"Events.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerEventList"}}}},"400":{"description":"`invalid_query`, `tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/partners/{id}/customers":{"get":{"operationId":"listPartnerCustomers","tags":["Partner reporting"],"summary":"Customers a partner brought in","description":"One row per attributed userId, by attributed revenue. Offset-paginated (default 50, max 200). A partner calling gets `userId: null` and the customer's `referralId` instead, and counts, dates and revenue over only the event types the brand shows as referral stages. For each customer's stages, see `GET /partners/{id}/referrals`.\n\n**Access:** admin key, a scoped key with `partners:read`, or the partner's own key (`:id` must be that partner).","security":[{"adminKey":[]},{"partnerKey":[]},{"scopedKey":["partners:read"]}],"x-openpartner-access":{"kind":"partnerOrAdmin","scopes":["partners:read"],"partner":true},"parameters":[{"schema":{"type":"string","description":"The id."},"required":true,"description":"The id.","name":"id","in":"path"},{"schema":{"type":"string","minLength":1},"required":false,"name":"programId","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200},"required":false,"name":"limit","in":"query"},{"schema":{"type":["integer","null"],"minimum":0},"required":false,"name":"offset","in":"query"}],"responses":{"200":{"description":"Customers.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerCustomerList"}}}},"400":{"description":"`invalid_query`, `tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/partners/{id}/breakdowns":{"get":{"operationId":"getPartnerBreakdown","tags":["Partner reporting"],"summary":"Top click sources by one dimension","description":"**Access:** admin key, a scoped key with `partners:read`, or the partner's own key (`:id` must be that partner).","security":[{"adminKey":[]},{"partnerKey":[]},{"scopedKey":["partners:read"]}],"x-openpartner-access":{"kind":"partnerOrAdmin","scopes":["partners:read"],"partner":true},"parameters":[{"schema":{"type":"string","description":"The id."},"required":true,"description":"The id.","name":"id","in":"path"},{"schema":{"type":"string","enum":["referer","utm_source","utm_medium","utm_campaign","country","device","browser","os"]},"required":true,"name":"dimension","in":"query"},{"schema":{"type":"string","minLength":1},"required":false,"name":"programId","in":"query"},{"schema":{"type":"string","format":"date-time","pattern":"Z$"},"required":false,"name":"since","in":"query"},{"schema":{"type":"string","format":"date-time","pattern":"Z$"},"required":false,"name":"until","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":50},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Top values.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerBreakdown"}}}},"400":{"description":"`invalid_query`, `tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/partners/{id}/funnel":{"get":{"operationId":"getPartnerFunnel","tags":["Partner reporting"],"summary":"Click → identity → signup → paid funnel","description":"One cohort: the partner's clicks since `since` (default: the last 30 days), and the users those clicks produced. Later stages count distinct users whose conversion was attributed to one of those clicks, so a customer won by an older click is never in it, renewals included. `signups` counts `signup`, `trial_started`, `subscription_created` and `invoice_paid` events and `paid` only `invoice_paid`, so report conversions under those types for this view to fill in. Each stage is a subset of the one before; rates are step over step.\n\n**Access:** admin key, or the partner's own key (`:id` must be that partner). Scoped keys get `403 forbidden`.","security":[{"adminKey":[]},{"partnerKey":[]}],"x-openpartner-access":{"kind":"partnerOrAdmin","scopes":[],"partner":true},"parameters":[{"schema":{"type":"string","description":"The id."},"required":true,"description":"The id.","name":"id","in":"path"},{"schema":{"type":"string","format":"date-time","description":"ISO-8601 start of the window. Default: 30 days ago. Not validated by the route: send a valid timestamp.","pattern":"Z$"},"required":false,"description":"ISO-8601 start of the window. Default: 30 days ago. Not validated by the route: send a valid timestamp.","name":"since","in":"query"}],"responses":{"200":{"description":"Funnel.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerFunnel"}}}},"400":{"description":"`invalid_query`, `tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/partners/{id}/referrals":{"get":{"operationId":"listPartnerReferrals","tags":["Partner reporting"],"summary":"A partner's referrals and the stages each has reached","description":"One row per customer the partner referred: a userId with at least one event credited to the partner, of any type, whether or not it earned anything. So a CRM stage that pays nothing (`lead_created`, `demo_booked`) still shows the partner their customer moving.\n\n- **Stages** are the event types the brand shows partners (`GET /config/referrals`), with its labels, in its order. Every other type is hidden: it still makes the customer a referral and still counts toward `commission`, but its type and dates never appear.\n- **No userId for partners.** A partner sees `referralId` (pseudonymous) and `label`: \"Referral #12\" (its `number`), or the `metadata.customerLabel` you send on events if you turn `showCustomerLabels` on. Admins and scoped keys also get `userId`, so a headless brand can show its own customer names.\n- **Reversals:** `commission` counts live commissions net of refund clawbacks; a stage whose commissions were all reversed or clawed back says `reversed: true` and is skipped by `latestStage`.\n- A revoked partner's referrals stay readable to the brand as history. The partner's own sessions and keys stop working when they're revoked, including keys minted for them afterwards.\n\nNewest referral first: the customer who most recently became the partner's referral leads, and `number` counts up in the same order. `referredAt` is when they first clicked the partner's link. Keyset-paginated: pass `nextCursor` back as `cursor`. The cursor is encrypted and carries no readable data.\n\n**Access:** admin key, a scoped key with `partners:read`, or the partner's own key (`:id` must be that partner).","security":[{"adminKey":[]},{"partnerKey":[]},{"scopedKey":["partners:read"]}],"x-openpartner-access":{"kind":"partnerOrAdmin","scopes":["partners:read"],"partner":true},"parameters":[{"schema":{"type":"string","description":"The id."},"required":true,"description":"The id.","name":"id","in":"path"},{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string"},"required":false,"name":"cursor","in":"query"}],"responses":{"200":{"description":"One page.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerReferralPage"}}}},"400":{"description":"`invalid_query`, `tenant_slug_missing`, `invalid_cursor`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/webhooks":{"post":{"operationId":"createWebhookEndpoint","tags":["Webhooks"],"summary":"Subscribe a webhook endpoint","description":"Returns the signing `secret` once. The URL must be publicly reachable over HTTP(S): a URL that resolves to a private, loopback or link-local address, `localhost`, or (hosted) a port other than 80/443 is refused with `400 destination_not_allowed`, whose `reason` names the rule (`destination_not_public`, `blocked_hostname`, `port_not_allowed`, …). Every delivery checks again, so a DNS change made later still cannot reach a private address. See **Webhook events** for the catalogue.\n\n**Access:** admin key, or a scoped key with `webhooks:write`.","security":[{"adminKey":[]},{"scopedKey":["webhooks:write"]}],"x-openpartner-access":{"kind":"admin","scopes":["webhooks:write"],"partner":false},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWebhookRequest"}}}},"responses":{"201":{"description":"Subscribed.","content":{"application/json":{"schema":{"type":"object","properties":{"endpoint":{"$ref":"#/components/schemas/WebhookEndpoint"},"secret":{"type":"string","description":"`whsec_…`. Shown once."}},"required":["endpoint","secret"]}}}},"400":{"description":"`invalid_body`, `tenant_slug_missing`, `destination_not_allowed`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"operationId":"listWebhookEndpoints","tags":["Webhooks"],"summary":"List webhook endpoints","description":"**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"responses":{"200":{"description":"Endpoints, including inactive ones.","content":{"application/json":{"schema":{"type":"object","properties":{"endpoints":{"type":"array","items":{"$ref":"#/components/schemas/WebhookEndpoint"}}},"required":["endpoints"]}}}},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/webhooks/{id}":{"get":{"operationId":"getWebhookEndpoint","tags":["Webhooks"],"summary":"Get a webhook endpoint","description":"**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"parameters":[{"schema":{"type":"string","description":"The id."},"required":true,"description":"The id.","name":"id","in":"path"}],"responses":{"200":{"description":"The endpoint.","content":{"application/json":{"schema":{"type":"object","properties":{"endpoint":{"$ref":"#/components/schemas/WebhookEndpoint"}},"required":["endpoint"]}}}},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"operationId":"updateWebhookEndpoint","tags":["Webhooks"],"summary":"Update a webhook endpoint","description":"A new `url` gets the same check as `createWebhookEndpoint`.\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"parameters":[{"schema":{"type":"string","description":"The id."},"required":true,"description":"The id.","name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWebhookRequest"}}}},"responses":{"200":{"description":"The updated endpoint.","content":{"application/json":{"schema":{"type":"object","properties":{"endpoint":{"$ref":"#/components/schemas/WebhookEndpoint"}},"required":["endpoint"]}}}},"400":{"description":"`invalid_body`, `tenant_slug_missing`, `destination_not_allowed`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"operationId":"deleteWebhookEndpoint","tags":["Webhooks"],"summary":"Unsubscribe a webhook endpoint","description":"Deactivates it and keeps its delivery log. `?hard=1` deletes the endpoint and its log.\n\n**Access:** admin key, or a scoped key with `webhooks:write`.","security":[{"adminKey":[]},{"scopedKey":["webhooks:write"]}],"x-openpartner-access":{"kind":"admin","scopes":["webhooks:write"],"partner":false},"parameters":[{"schema":{"type":"string","description":"The id."},"required":true,"description":"The id.","name":"id","in":"path"},{"schema":{"type":"string","enum":["1"],"description":"Not validated by the route: only `1` has an effect."},"required":false,"description":"Not validated by the route: only `1` has an effect.","name":"hard","in":"query"}],"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Ok"}}}},"400":{"description":"`invalid_query`, `tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/webhooks/{id}/secret":{"post":{"operationId":"rotateWebhookSecret","tags":["Webhooks"],"summary":"Rotate an endpoint's signing secret","description":"Returns the new `secret` once; the old one stops working immediately. Deliveries fail until your receiver has the new secret: retry them from the delivery log afterwards (`retryWebhookDelivery` signs with the current secret). Also how an endpoint restored from a SQL dump gets a secret you know.\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"parameters":[{"schema":{"type":"string","description":"The id."},"required":true,"description":"The id.","name":"id","in":"path"}],"responses":{"200":{"description":"Rotated.","content":{"application/json":{"schema":{"type":"object","properties":{"endpoint":{"$ref":"#/components/schemas/WebhookEndpoint"},"secret":{"type":"string","description":"`whsec_…`. Shown once."}},"required":["endpoint","secret"]}}}},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/webhooks/{id}/deliveries":{"get":{"operationId":"listWebhookDeliveries","tags":["Webhooks"],"summary":"List an endpoint's recent deliveries","description":"The 100 most recent, newest first. Failed deliveries are not retried automatically; use this to find them. Needs an admin key (`webhooks:write` only covers subscribing and unsubscribing).\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"parameters":[{"schema":{"type":"string","description":"The id."},"required":true,"description":"The id.","name":"id","in":"path"}],"responses":{"200":{"description":"Deliveries.","content":{"application/json":{"schema":{"type":"object","properties":{"deliveries":{"type":"array","items":{"$ref":"#/components/schemas/WebhookDelivery"}}},"required":["deliveries"]}}}},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/webhooks/{id}/test":{"post":{"operationId":"testWebhookEndpoint","tags":["Webhooks"],"summary":"Send a signed test event","description":"Sends a sample payload for `event` (or a generic `webhook.test`) to this endpoint regardless of its subscriptions, and returns the delivery. Samples have the real payload shape plus `_synthetic: true` and `_endpointId`.\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"parameters":[{"schema":{"type":"string","description":"The id."},"required":true,"description":"The id.","name":"id","in":"path"},{"schema":{"type":"string","enum":["attribution.created","commission.accrued","commission.approved","commission.paid","commission.reversed","bonus.awarded","bonus.approved","bonus.paid","bonus.reversed","partner.created","partner.activated","partnership.approved","payout.created"]},"required":false,"name":"event","in":"query"}],"responses":{"200":{"description":"The delivery attempt.","content":{"application/json":{"schema":{"type":"object","properties":{"delivery":{"anyOf":[{"$ref":"#/components/schemas/WebhookDelivery"},{"type":"null"}]},"event":{"type":"string"}},"required":["delivery","event"]}}}},"400":{"description":"`invalid_query`, `tenant_slug_missing`, `endpoint_inactive`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/webhooks/{id}/deliveries/{deliveryId}/retry":{"post":{"operationId":"retryWebhookDelivery","tags":["Webhooks"],"summary":"Redeliver a delivery","description":"Resends the same envelope (same `id`), freshly signed.\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"parameters":[{"schema":{"type":"string","description":"The id."},"required":true,"description":"The id.","name":"id","in":"path"},{"schema":{"type":"string","description":"The deliveryId."},"required":true,"description":"The deliveryId.","name":"deliveryId","in":"path"}],"responses":{"200":{"description":"The delivery after the attempt.","content":{"application/json":{"schema":{"type":"object","properties":{"delivery":{"$ref":"#/components/schemas/WebhookDelivery"}},"required":["delivery"]}}}},"400":{"description":"`tenant_slug_missing`, `endpoint_mismatch`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api-keys":{"post":{"operationId":"createAdminKey","tags":["API keys"],"summary":"Mint an unscoped admin key","description":"Full tenant access. Prefer a scoped key for integrations.\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateKeyRequest"}}}},"responses":{"201":{"description":"Minted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatedKey"}}}},"400":{"description":"`invalid_body`, `tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"operationId":"listScopedKeys","tags":["API keys"],"summary":"List scoped keys","description":"Scoped keys only (admin keys are not listed), including revoked ones. Never returns the key itself.\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"parameters":[{"schema":{"type":"string"},"required":false,"name":"scope","in":"query"}],"responses":{"200":{"description":"Keys.","content":{"application/json":{"schema":{"type":"object","properties":{"apiKeys":{"type":"array","items":{"$ref":"#/components/schemas/ApiKeySummary"}}},"required":["apiKeys"]}}}},"400":{"description":"`invalid_query`, `tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api-keys/scoped":{"post":{"operationId":"createScopedKey","tags":["API keys"],"summary":"Mint a scoped key","description":"A key that can call only the operations its scopes unlock (see **Authentication** in the introduction for the table). Scope strings are not validated: a misspelt scope silently unlocks nothing.\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateScopedKeyRequest"}}}},"responses":{"201":{"description":"Minted.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/CreatedKey"},{"type":"object","properties":{"scopes":{"type":"array","items":{"type":"string"}}},"required":["scopes"]}]}}}},"400":{"description":"`invalid_body`, `tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/partners/{id}/api-keys":{"post":{"operationId":"createPartnerKey","tags":["API keys"],"summary":"Mint a partner key","description":"A key that acts as this one partner: it reads and writes only that partner's own resources. Revoking the partner revokes its keys, and a key minted for a revoked partner doesn't authenticate (`401`) until they're reinstated.\n\n**Access:** admin key, or the partner's own key (`:id` must be that partner). Scoped keys get `403 forbidden`.","security":[{"adminKey":[]},{"partnerKey":[]}],"x-openpartner-access":{"kind":"partnerOrAdmin","scopes":[],"partner":true},"parameters":[{"schema":{"type":"string","description":"The id."},"required":true,"description":"The id.","name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"label":{"type":"string"}}}}}},"responses":{"201":{"description":"Minted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatedKey"}}}},"400":{"description":"`invalid_body`, `tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`partner_not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"operationId":"listPartnerKeys","tags":["API keys"],"summary":"List a partner's keys","description":"**Access:** admin key, or the partner's own key (`:id` must be that partner). Scoped keys get `403 forbidden`.","security":[{"adminKey":[]},{"partnerKey":[]}],"x-openpartner-access":{"kind":"partnerOrAdmin","scopes":[],"partner":true},"parameters":[{"schema":{"type":"string","description":"The id."},"required":true,"description":"The id.","name":"id","in":"path"}],"responses":{"200":{"description":"Keys.","content":{"application/json":{"schema":{"type":"object","properties":{"apiKeys":{"type":"array","items":{"$ref":"#/components/schemas/ApiKeySummary"}}},"required":["apiKeys"]}}}},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/auth/introspect":{"get":{"operationId":"introspectKey","tags":["API keys"],"summary":"Describe the calling key","description":"What the credential you sent can do: its scopes, the partner it is bound to, or unrestricted admin access. Useful when a call answers `403 forbidden_scope`.\n\n**Access:** any key or session.","security":[{"adminKey":[]},{"partnerKey":[]}],"x-openpartner-access":{"kind":"handler","scopes":[],"partner":true},"responses":{"200":{"description":"The caller.","content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"role":{"type":"string","enum":["scoped"]},"scopes":{"type":"array","items":{"type":"string"}}},"required":["role","scopes"]},{"type":"object","properties":{"role":{"type":"string","enum":["admin"]},"unrestricted":{"type":"boolean","enum":[true]}},"required":["role","unrestricted"]},{"type":"object","properties":{"role":{"type":"string","enum":["partner"]},"restrictedTo":{"type":"object","properties":{"partnerId":{"type":"string"}},"required":["partnerId"]}},"required":["role","restrictedTo"]}]}}}},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api-keys/{keyId}":{"delete":{"operationId":"revokeKey","tags":["API keys"],"summary":"Revoke a key","description":"**Access:** admin key (any key), or a partner key (that partner's own keys). Scoped keys get `403 forbidden`.","security":[{"adminKey":[]},{"partnerKey":[]}],"x-openpartner-access":{"kind":"handler","scopes":[],"partner":true},"parameters":[{"schema":{"type":"string","description":"The keyId."},"required":true,"description":"The keyId.","name":"keyId","in":"path"}],"responses":{"200":{"description":"Revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Ok"}}}},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/export/{table}.{format}":{"get":{"operationId":"exportTable","tags":["Export"],"summary":"Export one table","description":"JSON (array of rows), CSV, or a SQL dump. `tenantId` applies to `sql` only.\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"parameters":[{"schema":{"type":"string","enum":["Partner","Program","PartnerProgram","PartnerCommission","PartnerNetworkConsent","PartnerPayoutProfile","Coupon","Link","Click","ClickLanding","Identity","Event","Attribution","Bounty","Bonus","Commission","Payout","PortalCustomDomain","CommissionAdjustment","BrandAsset","WebhookEndpoint","PartnerPostback","ConversionPostbackToken","Config"],"description":"Table name."},"required":true,"description":"Table name.","name":"table","in":"path"},{"schema":{"type":"string","enum":["json","csv","sql"]},"required":true,"name":"format","in":"path"},{"schema":{"type":"string","description":"SQL only. Bake this literal tenant id into the dump instead of the portable `:tenant_id` psql variable."},"required":false,"description":"SQL only. Bake this literal tenant id into the dump instead of the portable `:tenant_id` psql variable.","name":"tenantId","in":"query"}],"responses":{"200":{"description":"The table.","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","additionalProperties":{}}}},"text/csv":{"schema":{"type":"string"}},"application/sql":{"schema":{"type":"string"}}}},"400":{"description":"`invalid_query`, `tenant_slug_missing`, `unsupported_format`, `invalid_tenant_id`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`table_not_exportable`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/export.json":{"get":{"operationId":"exportAllJson","tags":["Export"],"summary":"Export the whole tenant (JSON)","description":"**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"responses":{"200":{"description":"Every exportable table.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExportBundle"}}}},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/export.sql":{"get":{"operationId":"exportAllSql","tags":["Export"],"summary":"Export the whole tenant (SQL)","description":"Restore with `psql \"$DATABASE_URL\" -v tenant_id=<destination tenant id> -f openpartner-export.sql`.\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"parameters":[{"schema":{"type":"string","description":"Bake a literal tenant id instead of the psql variable."},"required":false,"description":"Bake a literal tenant id instead of the psql variable.","name":"tenantId","in":"query"}],"responses":{"200":{"description":"SQL dump.","content":{"application/sql":{"schema":{"type":"string"}}}},"400":{"description":"`invalid_query`, `tenant_slug_missing`, `invalid_tenant_id`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/import":{"post":{"operationId":"importBundle","tags":["Export"],"summary":"Import an export bundle (self-host only)","description":"Idempotent: rows whose primary key exists are skipped. Accepts bundles up to 256 MB on single-tenant installs (1 MB under a `/t/<slug>` path). Credentials are never in a bundle: restored webhook endpoints get a fresh signing secret and come back paused, and `report.credentials` shows those secrets once. See docs/data-portability.md.\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportRequest"}}}},"responses":{"200":{"description":"Rows inserted and skipped per table.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportResult"}}}},"400":{"description":"`invalid_body`, `tenant_slug_missing`, `unsupported_schema_version`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`, `import_disabled_on_hosted`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/config/network":{"get":{"operationId":"getNetworkMembership","tags":["Network settings"],"summary":"Get the tenant's OpenPartner Network membership","description":"`enabled && autoEnroll` means the tenant offers the Network to its partners: each is asked whether to join, and only those who opt in are shared. Nothing is shared for a partner who hasn't.\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"responses":{"200":{"description":"Membership.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NetworkMembership"}}}},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"updateNetworkMembership","tags":["Network settings"],"summary":"Update Network membership (e.g. stop offering the Network to partners)","description":"`autoEnroll` is the stored name for \"offer the Network to my partners\". Send `{\"autoEnroll\": false}` to stop asking partners and stop sharing anyone, whatever they chose. Partners already on the Network stay on it. Reconnecting the Network later (Admin → Network → Connect) turns `autoEnroll` back on, so re-check after any reconnect.\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateNetworkMembershipRequest"}}}},"responses":{"200":{"description":"The saved membership.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NetworkMembership"}}}},"400":{"description":"`invalid_body`, `tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`, `brand_pending_review`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`white_label_network_conflict`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/config/network/authorize-bonuses":{"post":{"operationId":"authorizeNetworkBonuses","tags":["Network settings"],"summary":"Let the OpenPartner Network pay bonuses","description":"Adds `bonuses:write` to the Network's existing federation key, so OpenPartner Network bounties can pay your partners through `POST /bonuses`. Keys issued since Network bounties launched already hold it (`bonusesAuthorized` on `GET /config/network`). An admin key or portal session only: a scoped key can't widen a key.\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"responses":{"200":{"description":"The membership, now with `bonusesAuthorized: true`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NetworkMembership"}}}},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`network_not_connected`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/config/partner-access":{"get":{"operationId":"getPartnerAccessSettings","tags":["Partners"],"summary":"Get the portal-access default for partners added without an invite","description":"**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"responses":{"200":{"description":"The setting.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerAccessSettings"}}}},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"updatePartnerAccessSettings","tags":["Partners"],"summary":"Set the portal-access default for partners added without an invite","description":"Send `{\"uninvitedPortalAccess\": false}` before a headless program creates its partners, so a partner created with `sendInvite: false` (or imported) can't sign in to the hosted portal unless the request says `portalAccess: true`. Applies to partners created from then on; change existing ones with `PATCH /partners/{id}`. Omitted fields keep their value. Also on Admin → Settings.\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePartnerAccessSettingsRequest"}}}},"responses":{"200":{"description":"The saved setting.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerAccessSettings"}}}},"400":{"description":"`invalid_body`, `tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/config/payout-statements":{"get":{"operationId":"getPayoutStatementSettings","tags":["Payouts"],"summary":"Get what payout statements print","description":"**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"responses":{"200":{"description":"The settings.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PayoutStatementSettings"}}}},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"updatePayoutStatementSettings","tags":["Payouts"],"summary":"Set what payout statements print","description":"Your legal name and address, a footer, and a prefix for statement numbers (`GET /payouts/{id}/statement`). Omitted fields keep their value; `\"\"` or `null` clears one. `taxLines` is a placeholder, off by default: OpenPartner records no tax amounts yet, so ask your accountant which lines (GST, TDS) you need before turning it on. Also on Admin → Settings.\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePayoutStatementSettingsRequest"}}}},"responses":{"200":{"description":"The saved settings.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PayoutStatementSettings"}}}},"400":{"description":"`invalid_body`, `tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/config/referrals":{"get":{"operationId":"getReferralSettings","tags":["Partner reporting"],"summary":"Get which stages partners see on their referrals","description":"**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"responses":{"200":{"description":"The setting.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReferralSettings"}}}},"400":{"description":"`tenant_slug_missing`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"updateReferralSettings","tags":["Partner reporting"],"summary":"Set which stages partners see on their referrals","description":"Sets the stages partners see on `GET /partners/{id}/referrals` and the partner portal's Referrals page: `{\"stages\": [{\"eventType\": \"lead_created\", \"label\": \"Lead\"}, {\"eventType\": \"demo_booked\", \"label\": \"Demo booked\"}, {\"eventType\": \"deal_won\", \"label\": \"Deal won\"}, {\"eventType\": \"invoice_paid\", \"label\": \"Paid\"}]}`. `stages` replaces the whole list, in the order given (at most 20; a label at most 60 characters). `refund`, `chargeback`, `dispute_created` and `invoice_payment_failed` are never attributed, so they can't be stages (`400 corrective_stage`); a type listed twice is `400 duplicate_stage`. `showCustomerLabels: true` shows partners the `metadata.customerLabel` you send on events. That is your decision as the data's controller, so it is off by default. Omitted fields keep their value. Also on Admin → Settings.\n\n**Access:** admin key only. Scoped keys get `403 forbidden`.","security":[{"adminKey":[]}],"x-openpartner-access":{"kind":"admin","scopes":[],"partner":false},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateReferralSettingsRequest"}}}},"responses":{"200":{"description":"The saved setting.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReferralSettings"}}}},"400":{"description":"`invalid_body`, `tenant_slug_missing`, `duplicate_stage`, `corrective_stage`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden`, `forbidden_scope`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"webhooks":{"attribution.created":{"post":{"operationId":"webhook_attribution_created","tags":["Webhook events"],"summary":"attribution.created","description":"A conversion event was credited to a partner. Fires once per credited partner (multi-touch models credit several).\n\nSubscribe with `events: [\"attribution.created\"]` (or `\"*\"`). Delivered as a signed JSON POST; see **Webhooks** in the introduction for verification.","parameters":[{"schema":{"type":"string","description":"Hex HMAC-SHA256 of `<timestamp>.<raw body>`, keyed with the endpoint secret."},"required":true,"description":"Hex HMAC-SHA256 of `<timestamp>.<raw body>`, keyed with the endpoint secret.","name":"x-openpartner-signature","in":"header"},{"schema":{"type":"string","description":"Unix seconds when this attempt was signed. Reject anything more than 5 minutes off."},"required":true,"description":"Unix seconds when this attempt was signed. Reject anything more than 5 minutes off.","name":"x-openpartner-timestamp","in":"header"},{"schema":{"type":"string","description":"The event type, e.g. `commission.accrued`."},"required":true,"description":"The event type, e.g. `commission.accrued`.","name":"x-openpartner-event","in":"header"},{"schema":{"type":"string","description":"The envelope `id`."},"required":true,"description":"The envelope `id`.","name":"x-openpartner-delivery","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Unique per event (`evt_…`). A manual redelivery reuses it, so dedupe on this.","example":"evt_01JABCDEF0123456789ABCDEFG"},"event":{"type":"string","enum":["attribution.created"]},"created":{"type":"string","format":"date-time","pattern":"Z$"},"data":{"type":"object","properties":{"attributionId":{"type":"string"},"eventId":{"type":"string"},"partnerId":{"type":"string"},"programId":{"type":"string"},"clickId":{"type":"string"},"model":{"type":"string","enum":["last_click","first_click","linear","position"]},"weight":{"type":"number","description":"This partner's share of the event, 0–1."},"eventType":{"type":"string"},"eventValue":{"anyOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}],"description":"The triggering event's `value` (decimal string), or null."},"commissionId":{"type":"string","description":"The first commission this attribution produced; omitted when it produced none. Listen to `commission.accrued` for every commission."},"commissionCurrency":{"type":"string"}},"required":["attributionId","eventId","partnerId","programId","clickId","model","weight","eventType","eventValue","commissionCurrency"]}},"required":["id","event","created","data"]}}}},"responses":{"200":{"description":"Any 2xx acknowledges. A non-2xx answer, a redirect, or no answer within 10 seconds marks the delivery failed. Failed deliveries are not retried automatically."}}}},"commission.accrued":{"post":{"operationId":"webhook_commission_accrued","tags":["Webhook events"],"summary":"commission.accrued","description":"A commission was written. One per commission rule that matched, so an event can accrue several.\n\nSubscribe with `events: [\"commission.accrued\"]` (or `\"*\"`). Delivered as a signed JSON POST; see **Webhooks** in the introduction for verification.","parameters":[{"schema":{"type":"string","description":"Hex HMAC-SHA256 of `<timestamp>.<raw body>`, keyed with the endpoint secret."},"required":true,"description":"Hex HMAC-SHA256 of `<timestamp>.<raw body>`, keyed with the endpoint secret.","name":"x-openpartner-signature","in":"header"},{"schema":{"type":"string","description":"Unix seconds when this attempt was signed. Reject anything more than 5 minutes off."},"required":true,"description":"Unix seconds when this attempt was signed. Reject anything more than 5 minutes off.","name":"x-openpartner-timestamp","in":"header"},{"schema":{"type":"string","description":"The event type, e.g. `commission.accrued`."},"required":true,"description":"The event type, e.g. `commission.accrued`.","name":"x-openpartner-event","in":"header"},{"schema":{"type":"string","description":"The envelope `id`."},"required":true,"description":"The envelope `id`.","name":"x-openpartner-delivery","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Unique per event (`evt_…`). A manual redelivery reuses it, so dedupe on this.","example":"evt_01JABCDEF0123456789ABCDEFG"},"event":{"type":"string","enum":["commission.accrued"]},"created":{"type":"string","format":"date-time","pattern":"Z$"},"data":{"type":"object","properties":{"commissionId":{"type":"string"},"partnerId":{"type":"string"},"attributionId":{"type":"string"},"clickId":{"type":"string"},"eventId":{"type":"string"},"programId":{"type":"string"},"amount":{"$ref":"#/components/schemas/Decimal"},"currency":{"type":"string"},"eventType":{"type":"string"},"eventValue":{"anyOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}],"description":"The triggering event's `value` (decimal string), or null."},"subId":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The publisher's own sub-ID from the partner link (`?sub_id=`), so a tracking tool can match the commission to its own click. Null when the link carried none."},"replacesCommissionId":{"type":"string","description":"Set when a partial refund reduced a commission: this one carries the amount the partner keeps, and replaces the commission named here (reversed in the `commission.reversed` sent just before)."},"sourceCommissionId":{"type":"string","description":"Set on a recruiter commission: the recruited partner's commission this one is a share of. `partnerId` is the recruiter; the attribution, click and event are the recruit's sale."},"recruitedPartnerId":{"type":"string","description":"Set on a recruiter commission: the recruited partner whose sale it was."}},"required":["commissionId","partnerId","attributionId","clickId","eventId","programId","amount","currency","eventType","eventValue","subId"]}},"required":["id","event","created","data"]}}}},"responses":{"200":{"description":"Any 2xx acknowledges. A non-2xx answer, a redirect, or no answer within 10 seconds marks the delivery failed. Failed deliveries are not retried automatically."}}}},"commission.approved":{"post":{"operationId":"webhook_commission_approved","tags":["Webhook events"],"summary":"commission.approved","description":"A commission was approved, by an admin or by the holdback auto-approve job.\n\nSubscribe with `events: [\"commission.approved\"]` (or `\"*\"`). Delivered as a signed JSON POST; see **Webhooks** in the introduction for verification.","parameters":[{"schema":{"type":"string","description":"Hex HMAC-SHA256 of `<timestamp>.<raw body>`, keyed with the endpoint secret."},"required":true,"description":"Hex HMAC-SHA256 of `<timestamp>.<raw body>`, keyed with the endpoint secret.","name":"x-openpartner-signature","in":"header"},{"schema":{"type":"string","description":"Unix seconds when this attempt was signed. Reject anything more than 5 minutes off."},"required":true,"description":"Unix seconds when this attempt was signed. Reject anything more than 5 minutes off.","name":"x-openpartner-timestamp","in":"header"},{"schema":{"type":"string","description":"The event type, e.g. `commission.accrued`."},"required":true,"description":"The event type, e.g. `commission.accrued`.","name":"x-openpartner-event","in":"header"},{"schema":{"type":"string","description":"The envelope `id`."},"required":true,"description":"The envelope `id`.","name":"x-openpartner-delivery","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Unique per event (`evt_…`). A manual redelivery reuses it, so dedupe on this.","example":"evt_01JABCDEF0123456789ABCDEFG"},"event":{"type":"string","enum":["commission.approved"]},"created":{"type":"string","format":"date-time","pattern":"Z$"},"data":{"type":"object","properties":{"commissionId":{"type":"string"},"partnerId":{"type":"string"},"attributionId":{"type":"string"},"amount":{"$ref":"#/components/schemas/Decimal"},"currency":{"type":"string"},"clickId":{"type":"string","description":"The click the commission traces back to. Omitted only if the origin lookup failed."},"eventId":{"type":"string"},"eventType":{"type":"string"},"programId":{"type":"string"},"subId":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The publisher's own sub-ID from the partner link (`?sub_id=`), so a tracking tool can match the commission to its own click. Null when the link carried none."},"eventValue":{"anyOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}],"description":"The triggering event's `value` (decimal string), or null."},"sourceCommissionId":{"type":"string","description":"Set on a recruiter commission: the recruited partner's commission this one is a share of. `partnerId` is the recruiter; the attribution, click and event are the recruit's sale."},"recruitedPartnerId":{"type":"string","description":"Set on a recruiter commission: the recruited partner whose sale it was."}},"required":["commissionId","partnerId","attributionId","amount","currency"]}},"required":["id","event","created","data"]}}}},"responses":{"200":{"description":"Any 2xx acknowledges. A non-2xx answer, a redirect, or no answer within 10 seconds marks the delivery failed. Failed deliveries are not retried automatically."}}}},"commission.paid":{"post":{"operationId":"webhook_commission_paid","tags":["Webhook events"],"summary":"commission.paid","description":"A commission was included in a payout. For a `manual` payout this fires when the payout is planned, before you have sent the money; `POST /payouts/{id}/confirm` records that you did.\n\nSubscribe with `events: [\"commission.paid\"]` (or `\"*\"`). Delivered as a signed JSON POST; see **Webhooks** in the introduction for verification.","parameters":[{"schema":{"type":"string","description":"Hex HMAC-SHA256 of `<timestamp>.<raw body>`, keyed with the endpoint secret."},"required":true,"description":"Hex HMAC-SHA256 of `<timestamp>.<raw body>`, keyed with the endpoint secret.","name":"x-openpartner-signature","in":"header"},{"schema":{"type":"string","description":"Unix seconds when this attempt was signed. Reject anything more than 5 minutes off."},"required":true,"description":"Unix seconds when this attempt was signed. Reject anything more than 5 minutes off.","name":"x-openpartner-timestamp","in":"header"},{"schema":{"type":"string","description":"The event type, e.g. `commission.accrued`."},"required":true,"description":"The event type, e.g. `commission.accrued`.","name":"x-openpartner-event","in":"header"},{"schema":{"type":"string","description":"The envelope `id`."},"required":true,"description":"The envelope `id`.","name":"x-openpartner-delivery","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Unique per event (`evt_…`). A manual redelivery reuses it, so dedupe on this.","example":"evt_01JABCDEF0123456789ABCDEFG"},"event":{"type":"string","enum":["commission.paid"]},"created":{"type":"string","format":"date-time","pattern":"Z$"},"data":{"type":"object","properties":{"commissionId":{"type":"string"},"partnerId":{"type":"string"},"attributionId":{"type":"string"},"amount":{"$ref":"#/components/schemas/Decimal"},"currency":{"type":"string"},"clickId":{"type":"string","description":"The click the commission traces back to. Omitted only if the origin lookup failed."},"eventId":{"type":"string"},"eventType":{"type":"string"},"programId":{"type":"string"},"subId":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The publisher's own sub-ID from the partner link (`?sub_id=`), so a tracking tool can match the commission to its own click. Null when the link carried none."},"eventValue":{"anyOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}],"description":"The triggering event's `value` (decimal string), or null."},"sourceCommissionId":{"type":"string","description":"Set on a recruiter commission: the recruited partner's commission this one is a share of. `partnerId` is the recruiter; the attribution, click and event are the recruit's sale."},"recruitedPartnerId":{"type":"string","description":"Set on a recruiter commission: the recruited partner whose sale it was."},"payoutId":{"type":"string"}},"required":["commissionId","partnerId","attributionId","amount","currency","payoutId"]}},"required":["id","event","created","data"]}}}},"responses":{"200":{"description":"Any 2xx acknowledges. A non-2xx answer, a redirect, or no answer within 10 seconds marks the delivery failed. Failed deliveries are not retried automatically."}}}},"commission.reversed":{"post":{"operationId":"webhook_commission_reversed","tags":["Webhook events"],"summary":"commission.reversed","description":"A commission was reversed: by an admin, or by a refund or chargeback, reported to `POST /attribution/refunds` or from Stripe's `charge.refunded`. A Stripe dispute reverses nothing until you report the chargeback.\n\nSubscribe with `events: [\"commission.reversed\"]` (or `\"*\"`). Delivered as a signed JSON POST; see **Webhooks** in the introduction for verification.","parameters":[{"schema":{"type":"string","description":"Hex HMAC-SHA256 of `<timestamp>.<raw body>`, keyed with the endpoint secret."},"required":true,"description":"Hex HMAC-SHA256 of `<timestamp>.<raw body>`, keyed with the endpoint secret.","name":"x-openpartner-signature","in":"header"},{"schema":{"type":"string","description":"Unix seconds when this attempt was signed. Reject anything more than 5 minutes off."},"required":true,"description":"Unix seconds when this attempt was signed. Reject anything more than 5 minutes off.","name":"x-openpartner-timestamp","in":"header"},{"schema":{"type":"string","description":"The event type, e.g. `commission.accrued`."},"required":true,"description":"The event type, e.g. `commission.accrued`.","name":"x-openpartner-event","in":"header"},{"schema":{"type":"string","description":"The envelope `id`."},"required":true,"description":"The envelope `id`.","name":"x-openpartner-delivery","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Unique per event (`evt_…`). A manual redelivery reuses it, so dedupe on this.","example":"evt_01JABCDEF0123456789ABCDEFG"},"event":{"type":"string","enum":["commission.reversed"]},"created":{"type":"string","format":"date-time","pattern":"Z$"},"data":{"type":"object","properties":{"commissionId":{"type":"string"},"partnerId":{"type":"string"},"attributionId":{"type":"string"},"amount":{"$ref":"#/components/schemas/Decimal"},"currency":{"type":"string"},"clickId":{"type":"string","description":"The click the commission traces back to. Omitted only if the origin lookup failed."},"eventId":{"type":"string"},"eventType":{"type":"string"},"programId":{"type":"string"},"subId":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The publisher's own sub-ID from the partner link (`?sub_id=`), so a tracking tool can match the commission to its own click. Null when the link carried none."},"eventValue":{"anyOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}],"description":"The triggering event's `value` (decimal string), or null."},"sourceCommissionId":{"type":"string","description":"Set on a recruiter commission: the recruited partner's commission this one is a share of. `partnerId` is the recruiter; the attribution, click and event are the recruit's sale."},"recruitedPartnerId":{"type":"string","description":"Set on a recruiter commission: the recruited partner whose sale it was."},"reason":{"type":"string","enum":["manual","refund","chargeback"],"description":"`manual` = an admin reversed it; `refund` = a refund (`POST /attribution/refunds`, or Stripe's `charge.refunded` from the self-host Stripe webhook or, on OpenPartner Cloud, the OpenPartner Stripe App); `chargeback` = a chargeback reported to `POST /attribution/refunds`."},"refundEventId":{"type":"string","description":"The refund or chargeback event behind a `refund` / `chargeback` reversal."},"replacementCommissionId":{"type":"string","description":"Partial refunds only: the commission accrued for the amount the partner keeps (sent next as `commission.accrued`)."}},"required":["commissionId","partnerId","attributionId","amount","currency","reason"]}},"required":["id","event","created","data"]}}}},"responses":{"200":{"description":"Any 2xx acknowledges. A non-2xx answer, a redirect, or no answer within 10 seconds marks the delivery failed. Failed deliveries are not retried automatically."}}}},"bonus.awarded":{"post":{"operationId":"webhook_bonus_awarded","tags":["Webhook events"],"summary":"bonus.awarded","description":"A bonus was awarded to a partner: a reward not tied to a sale, paid by an admin or integration (`POST /bonuses`), a performance bounty, or an OpenPartner Network bounty. Bonuses never fire `commission.*` events.\n\nSubscribe with `events: [\"bonus.awarded\"]` (or `\"*\"`). Delivered as a signed JSON POST; see **Webhooks** in the introduction for verification.","parameters":[{"schema":{"type":"string","description":"Hex HMAC-SHA256 of `<timestamp>.<raw body>`, keyed with the endpoint secret."},"required":true,"description":"Hex HMAC-SHA256 of `<timestamp>.<raw body>`, keyed with the endpoint secret.","name":"x-openpartner-signature","in":"header"},{"schema":{"type":"string","description":"Unix seconds when this attempt was signed. Reject anything more than 5 minutes off."},"required":true,"description":"Unix seconds when this attempt was signed. Reject anything more than 5 minutes off.","name":"x-openpartner-timestamp","in":"header"},{"schema":{"type":"string","description":"The event type, e.g. `commission.accrued`."},"required":true,"description":"The event type, e.g. `commission.accrued`.","name":"x-openpartner-event","in":"header"},{"schema":{"type":"string","description":"The envelope `id`."},"required":true,"description":"The envelope `id`.","name":"x-openpartner-delivery","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Unique per event (`evt_…`). A manual redelivery reuses it, so dedupe on this.","example":"evt_01JABCDEF0123456789ABCDEFG"},"event":{"type":"string","enum":["bonus.awarded"]},"created":{"type":"string","format":"date-time","pattern":"Z$"},"data":{"type":"object","properties":{"bonusId":{"type":"string"},"commissionId":{"type":"string","description":"The bonus's commission: the row payouts, approval and reversal act on. It has no `attributionId`."},"partnerId":{"type":"string"},"amount":{"$ref":"#/components/schemas/Decimal"},"currency":{"type":"string"},"programId":{"type":["string","null"],"description":"The program the bonus was paid under; its holdback applies. Null: none."},"bountyId":{"type":["string","null"],"description":"The performance bounty that awarded it, when `source` is `performance_bounty`."},"source":{"type":"string","enum":["manual","performance_bounty","network"],"description":"`manual`: an admin or an integration paid it (`POST /bonuses`). `performance_bounty`: the partner reached a bounty's goal. `network`: an OpenPartner Network bounty. New values may be added."},"note":{"type":["string","null"],"description":"The note shown to the partner."},"status":{"type":"string","enum":["accrued","approved"],"description":"`approved`: it is paid in the next payout run (and `bonus.approved` follows). `accrued`: held for the program's holdback first."}},"required":["bonusId","commissionId","partnerId","amount","currency","programId","bountyId","source","note","status"]}},"required":["id","event","created","data"]}}}},"responses":{"200":{"description":"Any 2xx acknowledges. A non-2xx answer, a redirect, or no answer within 10 seconds marks the delivery failed. Failed deliveries are not retried automatically."}}}},"bonus.approved":{"post":{"operationId":"webhook_bonus_approved","tags":["Webhook events"],"summary":"bonus.approved","description":"A bonus was approved for the next payout run: on award (`POST /bonuses` without `hold`, or a Network bonus), by an admin, or by the holdback auto-approve job.\n\nSubscribe with `events: [\"bonus.approved\"]` (or `\"*\"`). Delivered as a signed JSON POST; see **Webhooks** in the introduction for verification.","parameters":[{"schema":{"type":"string","description":"Hex HMAC-SHA256 of `<timestamp>.<raw body>`, keyed with the endpoint secret."},"required":true,"description":"Hex HMAC-SHA256 of `<timestamp>.<raw body>`, keyed with the endpoint secret.","name":"x-openpartner-signature","in":"header"},{"schema":{"type":"string","description":"Unix seconds when this attempt was signed. Reject anything more than 5 minutes off."},"required":true,"description":"Unix seconds when this attempt was signed. Reject anything more than 5 minutes off.","name":"x-openpartner-timestamp","in":"header"},{"schema":{"type":"string","description":"The event type, e.g. `commission.accrued`."},"required":true,"description":"The event type, e.g. `commission.accrued`.","name":"x-openpartner-event","in":"header"},{"schema":{"type":"string","description":"The envelope `id`."},"required":true,"description":"The envelope `id`.","name":"x-openpartner-delivery","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Unique per event (`evt_…`). A manual redelivery reuses it, so dedupe on this.","example":"evt_01JABCDEF0123456789ABCDEFG"},"event":{"type":"string","enum":["bonus.approved"]},"created":{"type":"string","format":"date-time","pattern":"Z$"},"data":{"type":"object","properties":{"bonusId":{"type":"string"},"commissionId":{"type":"string","description":"The bonus's commission: the row payouts, approval and reversal act on. It has no `attributionId`."},"partnerId":{"type":"string"},"amount":{"$ref":"#/components/schemas/Decimal"},"currency":{"type":"string"},"programId":{"type":["string","null"],"description":"The program the bonus was paid under; its holdback applies. Null: none."},"bountyId":{"type":["string","null"],"description":"The performance bounty that awarded it, when `source` is `performance_bounty`."},"source":{"type":"string","enum":["manual","performance_bounty","network"],"description":"`manual`: an admin or an integration paid it (`POST /bonuses`). `performance_bounty`: the partner reached a bounty's goal. `network`: an OpenPartner Network bounty. New values may be added."},"note":{"type":["string","null"],"description":"The note shown to the partner."}},"required":["bonusId","commissionId","partnerId","amount","currency"]}},"required":["id","event","created","data"]}}}},"responses":{"200":{"description":"Any 2xx acknowledges. A non-2xx answer, a redirect, or no answer within 10 seconds marks the delivery failed. Failed deliveries are not retried automatically."}}}},"bonus.paid":{"post":{"operationId":"webhook_bonus_paid","tags":["Webhook events"],"summary":"bonus.paid","description":"A bonus was included in a payout. For a `manual` payout this fires when the payout is planned, before you have sent the money, as for `commission.paid`.\n\nSubscribe with `events: [\"bonus.paid\"]` (or `\"*\"`). Delivered as a signed JSON POST; see **Webhooks** in the introduction for verification.","parameters":[{"schema":{"type":"string","description":"Hex HMAC-SHA256 of `<timestamp>.<raw body>`, keyed with the endpoint secret."},"required":true,"description":"Hex HMAC-SHA256 of `<timestamp>.<raw body>`, keyed with the endpoint secret.","name":"x-openpartner-signature","in":"header"},{"schema":{"type":"string","description":"Unix seconds when this attempt was signed. Reject anything more than 5 minutes off."},"required":true,"description":"Unix seconds when this attempt was signed. Reject anything more than 5 minutes off.","name":"x-openpartner-timestamp","in":"header"},{"schema":{"type":"string","description":"The event type, e.g. `commission.accrued`."},"required":true,"description":"The event type, e.g. `commission.accrued`.","name":"x-openpartner-event","in":"header"},{"schema":{"type":"string","description":"The envelope `id`."},"required":true,"description":"The envelope `id`.","name":"x-openpartner-delivery","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Unique per event (`evt_…`). A manual redelivery reuses it, so dedupe on this.","example":"evt_01JABCDEF0123456789ABCDEFG"},"event":{"type":"string","enum":["bonus.paid"]},"created":{"type":"string","format":"date-time","pattern":"Z$"},"data":{"type":"object","properties":{"bonusId":{"type":"string"},"commissionId":{"type":"string","description":"The bonus's commission: the row payouts, approval and reversal act on. It has no `attributionId`."},"partnerId":{"type":"string"},"amount":{"$ref":"#/components/schemas/Decimal"},"currency":{"type":"string"},"programId":{"type":["string","null"],"description":"The program the bonus was paid under; its holdback applies. Null: none."},"bountyId":{"type":["string","null"],"description":"The performance bounty that awarded it, when `source` is `performance_bounty`."},"source":{"type":"string","enum":["manual","performance_bounty","network"],"description":"`manual`: an admin or an integration paid it (`POST /bonuses`). `performance_bounty`: the partner reached a bounty's goal. `network`: an OpenPartner Network bounty. New values may be added."},"note":{"type":["string","null"],"description":"The note shown to the partner."},"payoutId":{"type":"string"}},"required":["bonusId","commissionId","partnerId","amount","currency","payoutId"]}},"required":["id","event","created","data"]}}}},"responses":{"200":{"description":"Any 2xx acknowledges. A non-2xx answer, a redirect, or no answer within 10 seconds marks the delivery failed. Failed deliveries are not retried automatically."}}}},"bonus.reversed":{"post":{"operationId":"webhook_bonus_reversed","tags":["Webhook events"],"summary":"bonus.reversed","description":"A bonus was reversed before it was paid. Paid bonuses are not clawed back.\n\nSubscribe with `events: [\"bonus.reversed\"]` (or `\"*\"`). Delivered as a signed JSON POST; see **Webhooks** in the introduction for verification.","parameters":[{"schema":{"type":"string","description":"Hex HMAC-SHA256 of `<timestamp>.<raw body>`, keyed with the endpoint secret."},"required":true,"description":"Hex HMAC-SHA256 of `<timestamp>.<raw body>`, keyed with the endpoint secret.","name":"x-openpartner-signature","in":"header"},{"schema":{"type":"string","description":"Unix seconds when this attempt was signed. Reject anything more than 5 minutes off."},"required":true,"description":"Unix seconds when this attempt was signed. Reject anything more than 5 minutes off.","name":"x-openpartner-timestamp","in":"header"},{"schema":{"type":"string","description":"The event type, e.g. `commission.accrued`."},"required":true,"description":"The event type, e.g. `commission.accrued`.","name":"x-openpartner-event","in":"header"},{"schema":{"type":"string","description":"The envelope `id`."},"required":true,"description":"The envelope `id`.","name":"x-openpartner-delivery","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Unique per event (`evt_…`). A manual redelivery reuses it, so dedupe on this.","example":"evt_01JABCDEF0123456789ABCDEFG"},"event":{"type":"string","enum":["bonus.reversed"]},"created":{"type":"string","format":"date-time","pattern":"Z$"},"data":{"type":"object","properties":{"bonusId":{"type":"string"},"commissionId":{"type":"string","description":"The bonus's commission: the row payouts, approval and reversal act on. It has no `attributionId`."},"partnerId":{"type":"string"},"amount":{"$ref":"#/components/schemas/Decimal"},"currency":{"type":"string"},"programId":{"type":["string","null"],"description":"The program the bonus was paid under; its holdback applies. Null: none."},"bountyId":{"type":["string","null"],"description":"The performance bounty that awarded it, when `source` is `performance_bounty`."},"source":{"type":"string","enum":["manual","performance_bounty","network"],"description":"`manual`: an admin or an integration paid it (`POST /bonuses`). `performance_bounty`: the partner reached a bounty's goal. `network`: an OpenPartner Network bounty. New values may be added."},"note":{"type":["string","null"],"description":"The note shown to the partner."},"reason":{"type":"string","enum":["manual","bounty_requalification"],"description":"`manual` = an admin reversed it; `bounty_requalification` = a performance bounty's goal was no longer met when its holdback ended (refunds took the partner back under it)."}},"required":["bonusId","commissionId","partnerId","amount","currency","reason"]}},"required":["id","event","created","data"]}}}},"responses":{"200":{"description":"Any 2xx acknowledges. A non-2xx answer, a redirect, or no answer within 10 seconds marks the delivery failed. Failed deliveries are not retried automatically."}}}},"partner.created":{"post":{"operationId":"webhook_partner_created","tags":["Webhook events"],"summary":"partner.created","description":"A partner was created: through `POST /partners` (admin, integration or Network approval; `source: \"api\"`), or by applying on the public join page (`source: \"join_page\"`). A join-page applicant held for review arrives with `activatedAt: null`; `POST /partners/{id}/invite` approves them. Not sent for partners imported with `POST /import/partners-csv`.\n\nSubscribe with `events: [\"partner.created\"]` (or `\"*\"`). Delivered as a signed JSON POST; see **Webhooks** in the introduction for verification.","parameters":[{"schema":{"type":"string","description":"Hex HMAC-SHA256 of `<timestamp>.<raw body>`, keyed with the endpoint secret."},"required":true,"description":"Hex HMAC-SHA256 of `<timestamp>.<raw body>`, keyed with the endpoint secret.","name":"x-openpartner-signature","in":"header"},{"schema":{"type":"string","description":"Unix seconds when this attempt was signed. Reject anything more than 5 minutes off."},"required":true,"description":"Unix seconds when this attempt was signed. Reject anything more than 5 minutes off.","name":"x-openpartner-timestamp","in":"header"},{"schema":{"type":"string","description":"The event type, e.g. `commission.accrued`."},"required":true,"description":"The event type, e.g. `commission.accrued`.","name":"x-openpartner-event","in":"header"},{"schema":{"type":"string","description":"The envelope `id`."},"required":true,"description":"The envelope `id`.","name":"x-openpartner-delivery","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Unique per event (`evt_…`). A manual redelivery reuses it, so dedupe on this.","example":"evt_01JABCDEF0123456789ABCDEFG"},"event":{"type":"string","enum":["partner.created"]},"created":{"type":"string","format":"date-time","pattern":"Z$"},"data":{"type":"object","properties":{"partnerId":{"type":"string"},"email":{"type":"string","format":"email"},"name":{"type":"string"},"activatedAt":{"type":["string","null"],"format":"date-time","description":"Null until the partner is active: an invited partner who has not accepted yet, or a join-page applicant held for review.","pattern":"Z$"},"invited":{"type":"boolean","description":"An invite email with a sign-in link was sent: `POST /partners` without `sendInvite: false`, or a join-page applicant approved automatically. An applicant held for review gets theirs when an admin approves them."},"portalAccess":{"type":"boolean","description":"The partner may sign in to the partner portal. See `portalAccess` on `POST /partners`."},"programIds":{"type":"array","items":{"type":"string"},"description":"The programs the partner was granted. Join-page applicants start with none."},"source":{"type":"string","enum":["api","join_page"],"description":"How the partner was added. `api`: `POST /partners` (the admin portal, an integration, or a Network approval, which also fires `partnership.approved`). `join_page`: they applied on the public join page. New values may be added."},"recruitedByPartnerId":{"type":"string","description":"Set when a partner recruited them: the one whose recruit link (`?ref=`) they joined through, or the one `POST /partners` named. Programs with a recruiter commission pay that partner a share of this one's commissions. Absent when nobody did."}},"required":["partnerId","email","name","activatedAt","invited","portalAccess","programIds","source"]}},"required":["id","event","created","data"]}}}},"responses":{"200":{"description":"Any 2xx acknowledges. A non-2xx answer, a redirect, or no answer within 10 seconds marks the delivery failed. Failed deliveries are not retried automatically."}}}},"partner.activated":{"post":{"operationId":"webhook_partner_activated","tags":["Webhook events"],"summary":"partner.activated","description":"A partner became active. Fires once per partner: when they accept their invite and sign in for the first time (`invited: true`), or right after `partner.created` for a partner who is active from the moment they are added (`invited: false`): `POST /partners` with `sendInvite: false`, or a join-page applicant the program approves automatically. A join-page applicant held for review fires it on their first sign-in after approval. `source` says which way they came in. Not sent for partners imported with `POST /import/partners-csv`.\n\nSubscribe with `events: [\"partner.activated\"]` (or `\"*\"`). Delivered as a signed JSON POST; see **Webhooks** in the introduction for verification.","parameters":[{"schema":{"type":"string","description":"Hex HMAC-SHA256 of `<timestamp>.<raw body>`, keyed with the endpoint secret."},"required":true,"description":"Hex HMAC-SHA256 of `<timestamp>.<raw body>`, keyed with the endpoint secret.","name":"x-openpartner-signature","in":"header"},{"schema":{"type":"string","description":"Unix seconds when this attempt was signed. Reject anything more than 5 minutes off."},"required":true,"description":"Unix seconds when this attempt was signed. Reject anything more than 5 minutes off.","name":"x-openpartner-timestamp","in":"header"},{"schema":{"type":"string","description":"The event type, e.g. `commission.accrued`."},"required":true,"description":"The event type, e.g. `commission.accrued`.","name":"x-openpartner-event","in":"header"},{"schema":{"type":"string","description":"The envelope `id`."},"required":true,"description":"The envelope `id`.","name":"x-openpartner-delivery","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Unique per event (`evt_…`). A manual redelivery reuses it, so dedupe on this.","example":"evt_01JABCDEF0123456789ABCDEFG"},"event":{"type":"string","enum":["partner.activated"]},"created":{"type":"string","format":"date-time","pattern":"Z$"},"data":{"type":"object","properties":{"partnerId":{"type":"string"},"email":{"type":"string","format":"email"},"name":{"type":"string"},"activatedAt":{"type":"string","format":"date-time","pattern":"Z$"},"invited":{"type":"boolean","description":"`true`: the partner accepted an invite and signed in for the first time. `false`: they were active from the moment they were added and have not signed in: `POST /partners` with `sendInvite: false` (`source: \"api\"`; they may have no portal access), or a join-page applicant approved automatically (`source: \"join_page\"`; they were emailed a sign-in link)."},"source":{"type":"string","enum":["api","join_page"],"description":"How the partner was added. `api`: `POST /partners` (the admin portal, an integration, or a Network approval, which also fires `partnership.approved`). `join_page`: they applied on the public join page. New values may be added."}},"required":["partnerId","email","name","activatedAt","invited","source"]}},"required":["id","event","created","data"]}}}},"responses":{"200":{"description":"Any 2xx acknowledges. A non-2xx answer, a redirect, or no answer within 10 seconds marks the delivery failed. Failed deliveries are not retried automatically."}}}},"partnership.approved":{"post":{"operationId":"webhook_partnership_approved","tags":["Webhook events"],"summary":"partnership.approved","description":"A partner was created from an approved OpenPartner Network application.\n\nSubscribe with `events: [\"partnership.approved\"]` (or `\"*\"`). Delivered as a signed JSON POST; see **Webhooks** in the introduction for verification.","parameters":[{"schema":{"type":"string","description":"Hex HMAC-SHA256 of `<timestamp>.<raw body>`, keyed with the endpoint secret."},"required":true,"description":"Hex HMAC-SHA256 of `<timestamp>.<raw body>`, keyed with the endpoint secret.","name":"x-openpartner-signature","in":"header"},{"schema":{"type":"string","description":"Unix seconds when this attempt was signed. Reject anything more than 5 minutes off."},"required":true,"description":"Unix seconds when this attempt was signed. Reject anything more than 5 minutes off.","name":"x-openpartner-timestamp","in":"header"},{"schema":{"type":"string","description":"The event type, e.g. `commission.accrued`."},"required":true,"description":"The event type, e.g. `commission.accrued`.","name":"x-openpartner-event","in":"header"},{"schema":{"type":"string","description":"The envelope `id`."},"required":true,"description":"The envelope `id`.","name":"x-openpartner-delivery","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Unique per event (`evt_…`). A manual redelivery reuses it, so dedupe on this.","example":"evt_01JABCDEF0123456789ABCDEFG"},"event":{"type":"string","enum":["partnership.approved"]},"created":{"type":"string","format":"date-time","pattern":"Z$"},"data":{"type":"object","properties":{"partnerId":{"type":"string"},"email":{"type":"string","format":"email"},"name":{"type":"string"},"networkCreatorId":{"description":"The creator's OpenPartner Network id, as the Network sent it."},"programIds":{"type":"array","items":{"type":"string"}}},"required":["partnerId","email","name","programIds"]}},"required":["id","event","created","data"]}}}},"responses":{"200":{"description":"Any 2xx acknowledges. A non-2xx answer, a redirect, or no answer within 10 seconds marks the delivery failed. Failed deliveries are not retried automatically."}}}},"payout.created":{"post":{"operationId":"webhook_payout_created","tags":["Webhook events"],"summary":"payout.created","description":"A payout was recorded: a Stripe Connect transfer that went out, or a `manual` payout you still have to send (it stays `pending` until confirmed).\n\nSubscribe with `events: [\"payout.created\"]` (or `\"*\"`). Delivered as a signed JSON POST; see **Webhooks** in the introduction for verification.","parameters":[{"schema":{"type":"string","description":"Hex HMAC-SHA256 of `<timestamp>.<raw body>`, keyed with the endpoint secret."},"required":true,"description":"Hex HMAC-SHA256 of `<timestamp>.<raw body>`, keyed with the endpoint secret.","name":"x-openpartner-signature","in":"header"},{"schema":{"type":"string","description":"Unix seconds when this attempt was signed. Reject anything more than 5 minutes off."},"required":true,"description":"Unix seconds when this attempt was signed. Reject anything more than 5 minutes off.","name":"x-openpartner-timestamp","in":"header"},{"schema":{"type":"string","description":"The event type, e.g. `commission.accrued`."},"required":true,"description":"The event type, e.g. `commission.accrued`.","name":"x-openpartner-event","in":"header"},{"schema":{"type":"string","description":"The envelope `id`."},"required":true,"description":"The envelope `id`.","name":"x-openpartner-delivery","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Unique per event (`evt_…`). A manual redelivery reuses it, so dedupe on this.","example":"evt_01JABCDEF0123456789ABCDEFG"},"event":{"type":"string","enum":["payout.created"]},"created":{"type":"string","format":"date-time","pattern":"Z$"},"data":{"type":"object","properties":{"payoutId":{"type":"string"},"partnerId":{"type":"string"},"amount":{"$ref":"#/components/schemas/Decimal"},"currency":{"type":"string"},"method":{"type":"string","enum":["stripe_connect","manual","external"]},"commissionIds":{"type":"array","items":{"type":"string"}},"platformFee":{"type":"number","description":"Revenue-share tier only: the platform fee noted on the payout (a ledger note, not a deduction)."},"grossAmount":{"allOf":[{"$ref":"#/components/schemas/Decimal"},{"description":"Present when refund clawbacks were netted off this payout: the sum of `commissionIds`, before them. `amount` = `grossAmount` − `clawbackAmount`, rounded to whole units in a currency Stripe pays without fractions (JPY, for example)."}]},"clawbackAmount":{"allOf":[{"$ref":"#/components/schemas/Decimal"},{"description":"Refund clawbacks deducted from this payout: commission the partner was already paid on revenue that was later refunded."}]},"clawbackAdjustmentIds":{"type":"array","items":{"type":"string"},"description":"The `refund_clawback` commission adjustments this payout deducted."},"statementUrl":{"type":"string","format":"uri","description":"The payout's statement: `GET /payouts/{id}/statement` on your API address. Fetch it with an admin key or a key holding `payouts:read`; add `?format=json` for its data. Absent on deliveries of payouts made before statements existed."}},"required":["payoutId","partnerId","amount","currency","method","commissionIds"]}},"required":["id","event","created","data"]}}}},"responses":{"200":{"description":"Any 2xx acknowledges. A non-2xx answer, a redirect, or no answer within 10 seconds marks the delivery failed. Failed deliveries are not retried automatically."}}}}}}