{"openapi":"3.1.0","info":{"title":"Refficks API","version":"1.0.0","description":"What your own server builds on: report events, and manage partners, programs, links and codes with an API key. Commissions and payouts are read only: no endpoint approves or pays one."},"servers":[{"url":"https://go.refficks.com/api/v1","description":"Production"}],"security":[{"apiKeyAuth":[]}],"paths":{"/events":{"post":{"tags":["Events"],"summary":"Ingest a normalized event","description":"The server-side ingest endpoint. Authenticated with an API key, not a\nsession.\n\nIdempotent: the `Idempotency-Key` header is required. Replaying the same\nkey with the same payload returns the original event and\n`\"status\": \"duplicate\"`. Reusing it with a different payload is a 409.\n\nProcessing is asynchronous: the response acknowledges receipt, it does\nnot report attribution or commission outcomes.\n","security":[{"apiKeyAuth":[]}],"parameters":[{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","maxLength":255}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventInput"}}}},"responses":{"202":{"description":"Accepted for processing.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"object":{"type":"string","const":"event"},"id":{"$ref":"#/components/schemas/Ulid"},"status":{"type":"string","enum":["accepted","duplicate"]}}}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"402":{"description":"`TRACKING_PAUSED`: the event's `occurred_at` (now, when it names none) falls while the organization's tracking was paused for being unpaid, so it was not recorded, and never will be. Refunds, disputes, credit notes and a dispute's end are recorded whenever they happened.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"422":{"$ref":"#/components/responses/ValidationError"}}}},"/server/me":{"get":{"tags":["Partners"],"summary":"Whose key this is (management API)","security":[{"apiKeyAuth":[]}],"description":"The organization the key belongs to, and the key's name,\npurpose (`zapier`, `make`, `n8n`, or null for a key made on the API\nkeys screen) and abilities, each with a label in words. An automation\nplatform shows this as the connection's name. Nothing in it is part of\nthe key, and any secret key may call it (`identify`).\n","responses":{"200":{"description":"`object: key_owner`, `organization {id, name, currency}`,\n`key {id, name, purpose, purpose_label, abilities[{ability, label}]}`,\n`dashboard_url`.\n"},"401":{"description":"No key, or a key that is revoked or expired."},"403":{"description":"A publishable key."}}}},"/server/hooks":{"get":{"tags":["Partners"],"summary":"List this key's subscriptions (management API)","security":[{"apiKeyAuth":[]}],"description":"The subscriptions this key made, oldest first, never the\nmerchant's own receivers and never another key's. Needs `hooks:manage`.\nEach has `url_hint` (host and last four characters), never the address.\n","responses":{"200":{"description":"`hook` objects: id, url_hint, events, status, status_label, ended_reason, last_delivered_at, ended_at, created_at."}}},"post":{"tags":["Partners"],"summary":"Subscribe to be told (management API)","security":[{"apiKeyAuth":[]}],"description":"What a REST-hook subscribe sends: `{url, event}` or\n`{url, events: [...]}`, optional `description`. The address is kept\nencrypted. Asking again for the same address from the same key answers\nthe same subscription (200, no secret) and starts it again if it had\nended; asking from another key while it is live is a 409. Every event\nneeds `partners:read`; commission and payout events need `money:read`\ntoo (403 naming the ability). At most 200 active per key.\n","responses":{"200":{"description":"The subscription this key already had for that address, and `secret` null."},"201":{"description":"The subscription, with its signing `secret`, shown once."},"403":{"description":"The key lacks `hooks:manage`, or the read ability the event needs."},"409":{"description":"Not an event Refficks sends; an address another key subscribed; the per-key limit; or an address Refficks will not send to."}}}},"/server/hooks/events":{"get":{"tags":["Partners"],"summary":"What may be subscribed to (management API)","security":[{"apiKeyAuth":[]}],"description":"Every event something in the product raises, with `label`,\n`needs` (the abilities it takes) and `subscribable` for this key: the\ndropdown an automation platform shows.\n","responses":{"200":{"description":"`hook_event` objects."}}}},"/server/hooks/samples":{"get":{"tags":["Partners"],"summary":"What an event looked like recently (management API)","security":[{"apiKeyAuth":[]}],"description":"Up to three of the merchant's own newest facts for `event`,\nnewest first, in the shape a delivery has (docs/events.md, \"Delivered\noutward\") and with the same `id` a live delivery of the same fact\ncarries. An empty list when there is nothing yet; nothing is invented.\nNeeds `hooks:manage` and the read ability the event carries.\n","parameters":[{"name":"event","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deliveries, each with id, event, event_label, occurred_at, partner, program, commission, payout, referral, lead and data."},"403":{"description":"The key lacks the read ability the event carries."},"422":{"description":"Not an event Refficks sends."}}}},"/server/hooks/{hook}":{"parameters":[{"name":"hook","in":"path","required":true,"schema":{"type":"string"}}],"get":{"tags":["Partners"],"summary":"One of this key's subscriptions (management API)","security":[{"apiKeyAuth":[]}],"responses":{"200":{"description":"The subscription."},"404":{"description":"Not a subscription this key made: another key's, a screen-made endpoint, or another account's."}}},"delete":{"tags":["Partners"],"summary":"Unsubscribe (management API)","security":[{"apiKeyAuth":[]}],"description":"Ends it and keeps it, with its delivery log; nothing more is sent. A\nsecond call is answered the same, because platforms retry.\n","responses":{"204":{"description":"Ended."},"404":{"description":"Not a subscription this key made."}}}},"/server/partners":{"get":{"tags":["Partners"],"summary":"List partners (management API)","description":"**Oldest first**, tie-broken on the key. A server walks the whole list,\nand a newest-first page shifts under a paginating caller every time a\npartner is added: page two repeats a row from page one and drops\nanother. Ascending by a key that only grows means a page already read\nstays read.\n","security":[{"apiKeyAuth":[]}],"parameters":[{"$ref":"#/components/parameters/Page"},{"$ref":"#/components/parameters/PerPage"},{"name":"status","in":"query","schema":{"$ref":"#/components/schemas/PartnerStatus"}},{"name":"lifecycle_stage","in":"query","schema":{"$ref":"#/components/schemas/PartnerLifecycleStage"}},{"name":"email","in":"query","description":"An exact address, case-insensitive.","schema":{"type":"string","format":"email"}}],"responses":{"200":{"description":"A page of partners.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/PaginatedEnvelope"},{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ServerPartner"}}}}]}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["Partners"],"summary":"Add a partner (management API)","description":"**Nobody is emailed unless the request says so.** No portal invitation\nis sent by default; pass `send_portal_invitation: true` for one. A\nperson pressing Add Partner is adding one person and watching; a server\nis a loop, and a first sync that mails two thousand people (several\nhundred of whom left years ago) cannot be un-sent.\n\n`organization_id` is never accepted. The tenant comes from the key, and\nsupplying it is a validation error rather than a silently ignored field.\n\n`status` is refused. A partner created already archived would have a\nstatus nobody set and no audit entry could explain; archiving is its\nown call.\n","security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email"},"first_name":{"type":"string","nullable":true},"last_name":{"type":"string","nullable":true},"company_name":{"type":"string","nullable":true},"phone":{"type":"string","nullable":true},"country":{"type":"string","minLength":2,"maxLength":2,"description":"ISO 3166-1 alpha-2, checked against the list. Case does not matter.","examples":["GB"]},"timezone":{"type":"string","nullable":true},"website":{"type":"string","format":"uri","nullable":true},"roles":{"type":"array","items":{"$ref":"#/components/schemas/PartnerRole"}},"tags":{"type":"array","items":{"type":"string","maxLength":64}},"lifecycle_stage":{"type":"string","enum":["applicant","approved"],"default":"approved","description":"Only the two stages that mean something at the moment of\narrival. The later ones (onboarded, growing, dormant, at\nrisk) are conclusions drawn from what a partner did, and a\nrecord with no history cannot be one. They are reached\nthrough `/transition`.\n"},"send_portal_invitation":{"type":"boolean","default":false}}}}}},"responses":{"201":{"description":"The partner.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ServerPartner"}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"402":{"$ref":"#/components/responses/EntitlementExceeded"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"A partner with that address already exists here."},"422":{"$ref":"#/components/responses/ValidationError"}}}},"/server/partners/lookup":{"get":{"tags":["Partners"],"summary":"Find one partner by something you already have","description":"Exactly one of `email` or `code`. Both, or neither, is a 422: \"either,\nand I will guess\" is how a lookup quietly answers the wrong question\nwhen the two disagree.\n\nA single object or a 404, never a list of length zero or one. An\nintegration should not have to store Refficks' ids to use Refficks, and\nshould not have to unwrap an array to find that out.\n\n`code` matches any tracking link of theirs, including a retired one: a\ncode out of a year-old newsletter still identifies whose it was.\n","security":[{"apiKeyAuth":[]}],"parameters":[{"name":"email","in":"query","schema":{"type":"string","format":"email"}},{"name":"code","in":"query","schema":{"type":"string","maxLength":120}}],"responses":{"200":{"description":"The partner.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ServerPartner"}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Nothing here matches that."},"422":{"$ref":"#/components/responses/ValidationError"}}}},"/server/partners/{partner}":{"parameters":[{"$ref":"#/components/parameters/PartnerId"}],"get":{"tags":["Partners"],"summary":"Read one partner (management API)","security":[{"apiKeyAuth":[]}],"responses":{"200":{"description":"The partner.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ServerPartner"}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"tags":["Partners"],"summary":"Change a partner's details (management API)","description":"A genuine partial update: a field sent as `null` is cleared, a field not\nsent is untouched.\n\nDetails only. `status`, `lifecycle_stage`, `roles` and `currency` are\n**refused** with a message naming the endpoint that does the job:\nrefused rather than ignored, because a silently dropped field returns a\n200 and a partner object, and the caller ships. The bug then surfaces\nmonths later against the one partner where it mattered.\n\nThe scores are refused too. They are computed, and a score a client can\nset is a score no recommendation built on it means anything.\n","security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"not":{},"description":"Refused with a 422. A partner's email is also how they sign in,\nto every merchant they work with, so Refficks support changes\nit rather than a merchant.\n"},"first_name":{"type":"string","nullable":true},"last_name":{"type":"string","nullable":true},"company_name":{"type":"string","nullable":true},"phone":{"type":"string","nullable":true},"country":{"type":"string","minLength":2,"maxLength":2,"nullable":true},"timezone":{"type":"string","nullable":true},"website":{"type":"string","format":"uri","nullable":true}}}}}},"responses":{"200":{"description":"The partner, as it now stands.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ServerPartner"}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Another partner here already uses that address."},"422":{"$ref":"#/components/responses/ValidationError"}}}},"/server/partners/{partner}/transition":{"parameters":[{"$ref":"#/components/parameters/PartnerId"}],"post":{"tags":["Partners"],"summary":"Move a partner between lifecycle stages (management API)","description":"Separate from `PATCH` because a stage change is a decision with\nconsequences (it fires automations, it stamps `approved_at`, it is\nwhat lets somebody into the portal), and an edit to a field is not.\n\nThe legal moves are the same ones a person gets. An illegal one is a\n409, so a server cannot walk a partner around the funnel a screen would\nrefuse to.\n","security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["lifecycle_stage"],"properties":{"lifecycle_stage":{"$ref":"#/components/schemas/PartnerLifecycleStage"},"reason":{"type":"string","maxLength":255,"nullable":true}}}}}},"responses":{"200":{"description":"The partner, at their new stage.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ServerPartner"}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"That move is not one this stage allows."},"422":{"$ref":"#/components/responses/ValidationError"}}}},"/server/partners/{partner}/archive":{"parameters":[{"$ref":"#/components/parameters/PartnerId"}],"post":{"tags":["Partners"],"summary":"Archive a partner (management API)","description":"Archiving, not deleting. A partner is attached to conversions, ledger\nentries and payouts; removing the row would make last quarter\nunreconcilable. It also stops their tracking: an archived partner's\nlinks stop earning.\n\nSecurity review F-25: refused with 403 to a key made for an automation\nplatform, and so is restoring.\n\nA POST to a named path rather than a `DELETE`, because `DELETE`\npromises removal and this keeps everything.\n\n**Erasure is deliberately absent from this surface.** It takes a typed\nconfirmation from somebody who has read the sentence saying it cannot be\nundone, and a token cannot read a sentence.\n","security":[{"apiKeyAuth":[]}],"responses":{"200":{"description":"The partner, now archived.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ServerPartner"}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/server/partners/{partner}/restore":{"parameters":[{"$ref":"#/components/parameters/PartnerId"}],"post":{"tags":["Partners"],"summary":"Put an archived partner back (management API)","description":"Their programme memberships are deliberately untouched, here as on the\nscreen: restoring must not reinstate a programme the merchant suspended\non purpose before archiving them. Two decisions, and only one of them is\nbeing undone.\n","security":[{"apiKeyAuth":[]}],"responses":{"200":{"description":"The partner, active again.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ServerPartner"}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/server/programs":{"get":{"tags":["Programs"],"summary":"List programmes (management API)","security":[{"apiKeyAuth":[]}],"parameters":[{"$ref":"#/components/parameters/Page"},{"$ref":"#/components/parameters/PerPage"},{"name":"status","in":"query","schema":{"$ref":"#/components/schemas/ProgramStatus"}},{"name":"module","in":"query","schema":{"$ref":"#/components/schemas/ProgramModule"}}],"responses":{"200":{"description":"A page of programmes, oldest first.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/PaginatedEnvelope"},{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ServerProgram"}}}}]}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["Programs"],"summary":"Create a programme (management API)","description":"`slug` is derived from the name and cannot be chosen here: uniqueness\nis per organization and there is nothing to check against until the\nprogramme exists. It is editable afterwards.\n\nSet `default_destination_url`: without it every link you later mint has\nto carry the same URL again, and a link with nowhere to go is refused.\n","security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","maxLength":255},"description":{"type":"string","nullable":true,"maxLength":2000},"module":{"$ref":"#/components/schemas/ProgramModule"},"status":{"$ref":"#/components/schemas/ProgramStatus"},"default_currency":{"type":"string","minLength":3,"maxLength":3},"attribution_window_days":{"type":"integer","minimum":1,"maximum":365,"description":"How long after a click a sale still credits the partner, what other platforms call the cookie duration."},"cookie_window_days":{"not":{},"description":"Removed. Refused with a 422 naming `attribution_window_days`, which is the one window a programme has."},"auto_approve_partners":{"type":"boolean"},"terms_url":{"type":"string","format":"uri","nullable":true},"default_destination_url":{"type":"string","format":"uri","nullable":true}}}}}},"responses":{"201":{"description":"The programme.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ServerProgram"}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"402":{"$ref":"#/components/responses/EntitlementExceeded"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"The module this programme needs is not enabled here."},"422":{"$ref":"#/components/responses/ValidationError"}}}},"/server/programs/{program}":{"parameters":[{"$ref":"#/components/parameters/ProgramId"}],"get":{"tags":["Programs"],"summary":"Read one programme (management API)","security":[{"apiKeyAuth":[]}],"responses":{"200":{"description":"The programme.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ServerProgram"}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"tags":["Programs"],"summary":"Change a programme's settings (management API)","description":"`module` is refused: it decides which half of the product a programme\nbelongs to, so changing it would leave everything already recorded\nunder the old one belonging to nothing.\n\nRates are not here: a key may set no rate, so commission plans are\nread only.\n","security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","maxLength":255},"description":{"type":"string","nullable":true,"maxLength":2000},"slug":{"type":"string","maxLength":60},"status":{"$ref":"#/components/schemas/ProgramStatus"},"default_currency":{"type":"string","minLength":3,"maxLength":3},"attribution_window_days":{"type":"integer","minimum":1,"maximum":365,"description":"How long after a click a sale still credits the partner, what other platforms call the cookie duration."},"cookie_window_days":{"not":{},"description":"Removed. Refused with a 422 naming `attribution_window_days`, which is the one window a programme has."},"auto_approve_partners":{"type":"boolean"},"terms_url":{"type":"string","format":"uri","nullable":true},"default_destination_url":{"type":"string","format":"uri","nullable":true}}}}}},"responses":{"200":{"description":"The programme, as it now stands.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ServerProgram"}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationError"}}}},"/server/programs/{program}/partners":{"parameters":[{"$ref":"#/components/parameters/ProgramId"}],"get":{"tags":["Programs"],"summary":"Who is in a programme (management API)","security":[{"apiKeyAuth":[]}],"parameters":[{"$ref":"#/components/parameters/Page"},{"$ref":"#/components/parameters/PerPage"},{"name":"status","in":"query","schema":{"$ref":"#/components/schemas/ProgramPartnerStatus"}},{"name":"partner_id","in":"query","schema":{"$ref":"#/components/schemas/Ulid"}}],"responses":{"200":{"description":"A page of memberships.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/PaginatedEnvelope"},{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ServerProgramMembership"}}}}]}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}},"post":{"tags":["Programs"],"summary":"Enrol a partner (management API)","description":"A tier may be chosen here and not changed afterwards: a tier sets what\nsomebody earns, and a key may set no rate, so a `PATCH` naming one is\nrefused.\n\n**Two things to know before your first script.** A partner created\nwhile the merchant has exactly one programme is already in it, so\nenrolling them again is a `409`; enrolment is deliberately not\nidempotent, because a second call that meant to change something and\nsilently did nothing is worse than a conflict you can read. And\nenrolling somebody mints their first tracking link.\n","security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["partner_id"],"properties":{"partner_id":{"$ref":"#/components/schemas/Ulid"},"tier_id":{"allOf":[{"$ref":"#/components/schemas/Ulid"}],"nullable":true}}}}}},"responses":{"201":{"description":"The membership.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ServerProgramMembership"}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"That partner is already in this programme."},"422":{"$ref":"#/components/responses/ValidationError"}}}},"/server/programs/{program}/partners/{membership}":{"parameters":[{"$ref":"#/components/parameters/ProgramId"},{"name":"membership","in":"path","required":true,"schema":{"$ref":"#/components/schemas/Ulid"}}],"patch":{"tags":["Programs"],"summary":"Suspend a membership, or restore it (management API)","description":"`status` only. `tier_id` and `custom_commission_plan_id` are refused\nwith a message naming where they are set. Both decide what somebody\nearns, and a key may set no rate.\n","security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["status"],"properties":{"status":{"$ref":"#/components/schemas/ProgramPartnerStatus"}}}}}},"responses":{"200":{"description":"The membership, as it now stands.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ServerProgramMembership"}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationError"}}}},"/server/programs/{program}/commission-plans":{"parameters":[{"$ref":"#/components/parameters/ProgramId"}],"get":{"tags":["Commissions"],"summary":"What a programme pays (management API, read only)","description":"**There is no POST here, and the absence is the decision.** A plan sets\nwhat everybody earns, and nobody has said a token may set a rate. A\nwrite is a `405`: the address is real and the method is not.\n\nOrdered by `priority`, which is the order that decides which plan pays:\na merchant reading this list is almost always asking which one\napplies.\n","security":[{"apiKeyAuth":[]}],"parameters":[{"$ref":"#/components/parameters/Page"},{"$ref":"#/components/parameters/PerPage"}],"responses":{"200":{"description":"A page of plans, highest priority first.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/PaginatedEnvelope"},{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ServerCommissionPlan"}}}}]}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"description":"This API does not write what anybody earns."}}}},"/server/programs/{program}/commission-plans/{plan}":{"parameters":[{"$ref":"#/components/parameters/ProgramId"},{"name":"plan","in":"path","required":true,"schema":{"$ref":"#/components/schemas/Ulid"}}],"get":{"tags":["Commissions"],"summary":"Read one commission plan (management API, read only)","security":[{"apiKeyAuth":[]}],"responses":{"200":{"description":"The plan.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ServerCommissionPlan"}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"description":"This API does not write what anybody earns."}}}},"/server/tracking-links":{"get":{"tags":["Tracking"],"summary":"List tracking links (management API)","security":[{"apiKeyAuth":[]}],"parameters":[{"$ref":"#/components/parameters/Page"},{"$ref":"#/components/parameters/PerPage"},{"name":"partner_id","in":"query","schema":{"$ref":"#/components/schemas/Ulid"}},{"name":"program_id","in":"query","schema":{"$ref":"#/components/schemas/Ulid"}},{"name":"status","in":"query","schema":{"$ref":"#/components/schemas/TrackingStatus"}},{"name":"code","in":"query","description":"An exact code.","schema":{"type":"string","maxLength":120}}],"responses":{"200":{"description":"A page of links, oldest first.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/PaginatedEnvelope"},{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ServerTrackingLink"}}}}]}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["Tracking"],"summary":"Mint a link for a partner (management API)","description":"The partner must already be in the programme: a link's terms come from\nthe membership, so a link without one is a link nobody could state the\nrate for. Enrolling a partner also mints their first link, so this is\nfor the second and after.\n\nWith no `destination_url`, the programme's `default_destination_url` is\nused. With neither, the request is refused.\n","security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["program_id","partner_id"],"properties":{"program_id":{"$ref":"#/components/schemas/Ulid"},"partner_id":{"$ref":"#/components/schemas/Ulid"},"code":{"type":"string","maxLength":64,"nullable":true},"name":{"type":"string","maxLength":255,"nullable":true},"destination_url":{"type":"string","format":"uri","nullable":true},"attribution_window_days":{"type":"integer","minimum":1,"maximum":365,"nullable":true,"description":"Overrides the programme's window for this link alone."}}}}}},"responses":{"201":{"description":"The link.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ServerTrackingLink"}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The partner is not in the programme, the code is taken, or the link has nowhere to go."},"422":{"$ref":"#/components/responses/ValidationError"}}}},"/server/tracking-links/{link}":{"parameters":[{"name":"link","in":"path","required":true,"schema":{"$ref":"#/components/schemas/Ulid"}}],"get":{"tags":["Tracking"],"summary":"Read one link (management API)","security":[{"apiKeyAuth":[]}],"responses":{"200":{"description":"The link.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ServerTrackingLink"}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"tags":["Tracking"],"summary":"Rename, repoint or retire a link (management API)","description":"**`code` is refused.** It is printed in videos, bios and newsletters,\nso renaming it would silently break every place a partner already\nshared it, and a script could do that to two hundred people in one\ncall. Retire this link (`status: archived`) and mint another.\n\n`partner_id` and `program_id` are refused too: moving a link would\nreassign the clicks it has already recorded.\n","security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","maxLength":255,"nullable":true},"destination_url":{"type":"string","format":"uri"},"attribution_window_days":{"type":"integer","minimum":1,"maximum":365,"nullable":true},"status":{"$ref":"#/components/schemas/TrackingStatus"}}}}}},"responses":{"200":{"description":"The link, as it now stands.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ServerTrackingLink"}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationError"}}}},"/server/coupon-codes":{"get":{"tags":["Tracking"],"summary":"List coupon codes (management API)","security":[{"apiKeyAuth":[]}],"parameters":[{"$ref":"#/components/parameters/Page"},{"$ref":"#/components/parameters/PerPage"},{"name":"partner_id","in":"query","schema":{"$ref":"#/components/schemas/Ulid"}},{"name":"program_id","in":"query","schema":{"$ref":"#/components/schemas/Ulid"}},{"name":"status","in":"query","schema":{"$ref":"#/components/schemas/TrackingStatus"}},{"name":"code","in":"query","schema":{"type":"string","maxLength":120}}],"responses":{"200":{"description":"A page of codes, oldest first.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/PaginatedEnvelope"},{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ServerCouponCode"}}}}]}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["Tracking"],"summary":"Make a code for a partner (management API)","description":"**This does not create a discount in your payment provider.** Refficks\nconnects to Stripe as a platform and holds no credential of yours. What\nthis creates is the record that says *this code belongs to this\npartner*.\n\nTo register a promotion code you already made, send\n`external_promotion_id`, with `account_id` naming which connected\naccount holds it when you have more than one.\n\nSecurity review F-15: Stripe is asked what the promotion's code is,\nand a claim whose code is not this code is refused with 422; a\npromotion another code already claims is refused with 409. A key made\nfor an automation platform cannot claim one at all (403), nor switch a\ncode or link on or off.\n\n`code` is optional: left out, one is derived from the partner's name,\nwhich is what a server looping over two hundred partners wants.\n\nOne code per partner per programme, unless the merchant has turned on\nmore than one. A second is a `409`.\n","security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["program_id","partner_id"],"properties":{"program_id":{"$ref":"#/components/schemas/Ulid"},"partner_id":{"$ref":"#/components/schemas/Ulid"},"code":{"type":"string","maxLength":64,"nullable":true},"provider":{"type":"string","maxLength":64,"nullable":true},"attribution_priority":{"type":"integer","minimum":0,"maximum":1000},"external_promotion_id":{"type":"string","maxLength":255,"nullable":true},"account_id":{"type":"string","maxLength":255,"nullable":true,"description":"The `acct_…` id of the connected account holding that promotion code."}}}}}},"responses":{"201":{"description":"The code.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ServerCouponCode"}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"That code is taken, or this partner already has one here."},"422":{"$ref":"#/components/responses/ValidationError"}}}},"/server/coupon-codes/{coupon}":{"parameters":[{"name":"coupon","in":"path","required":true,"schema":{"$ref":"#/components/schemas/Ulid"}}],"get":{"tags":["Tracking"],"summary":"Read one code (management API)","security":[{"apiKeyAuth":[]}],"responses":{"200":{"description":"The code.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ServerCouponCode"}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"tags":["Tracking"],"summary":"Change a code's limits or where it is placed (management API)","description":"`code` is refused, for the reason a link's is: it is what somebody\ntypes at the checkout. Retire this one and make another.\n\n`uses_count` is refused because it is Refficks' count of redemptions:\na merchant who could edit it could hand somebody another fifty uses\nwith no record of having done so.\n\nSending `external_promotion_id: null` takes a placement back, which is\na different instruction from not sending the field at all.\n","security":[{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"$ref":"#/components/schemas/TrackingStatus"},"attribution_priority":{"type":"integer","minimum":0,"maximum":1000},"max_uses":{"type":"integer","minimum":1,"nullable":true},"expires_at":{"type":"string","format":"date-time","nullable":true},"minimum_amount":{"type":"integer","minimum":0,"nullable":true,"description":"Minor units. Null means the programme's default applies, not \"no minimum\"."},"minimum_amount_currency":{"type":"string","minLength":3,"maxLength":3,"nullable":true},"first_time_transaction":{"type":"boolean","nullable":true},"external_promotion_id":{"type":"string","maxLength":255,"nullable":true},"account_id":{"type":"string","maxLength":255,"nullable":true}}}}}},"responses":{"200":{"description":"The code, as it now stands.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ServerCouponCode"}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationError"}}}},"/server/commissions":{"get":{"tags":["Commissions"],"summary":"Read the ledger (management API, read only)","description":"`occurred_from` / `occurred_to` filter on `occurred_at`, not\n`created_at`: a sale in March recorded in April belongs to March's\nbooks.\n\nPaged by `id`, because a ULID only grows while `occurred_at` does not:\na late-arriving event carries an older date and would insert rows\nbehind a crawler.\n\n**No total.** A ledger holds several currencies and this API will not\nsum across them; a figure nobody can spend is worse than none.\n","security":[{"apiKeyAuth":[]}],"parameters":[{"$ref":"#/components/parameters/Page"},{"$ref":"#/components/parameters/PerPage"},{"name":"partner_id","in":"query","schema":{"$ref":"#/components/schemas/Ulid"}},{"name":"program_id","in":"query","schema":{"$ref":"#/components/schemas/Ulid"}},{"name":"payout_id","in":"query","schema":{"$ref":"#/components/schemas/Ulid"}},{"name":"status","in":"query","schema":{"$ref":"#/components/schemas/CommissionEntryStatus"}},{"name":"type","in":"query","schema":{"$ref":"#/components/schemas/CommissionEntryType"}},{"name":"currency","in":"query","schema":{"type":"string","minLength":3,"maxLength":3}},{"name":"occurred_from","in":"query","schema":{"type":"string","format":"date"}},{"name":"occurred_to","in":"query","schema":{"type":"string","format":"date"}}],"responses":{"200":{"description":"A page of ledger entries.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/PaginatedEnvelope"},{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ServerCommissionEntry"}}}}]}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"405":{"description":"This API does not decide money."}}}},"/server/commissions/{entry}":{"parameters":[{"name":"entry","in":"path","required":true,"schema":{"$ref":"#/components/schemas/Ulid"}}],"get":{"tags":["Commissions"],"summary":"Read one ledger entry (management API, read only)","security":[{"apiKeyAuth":[]}],"responses":{"200":{"description":"The entry.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ServerCommissionEntry"}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"description":"This API does not decide money."}}}},"/server/payouts":{"get":{"tags":["Payouts"],"summary":"Read payouts (management API, read only)","description":"**There is no POST.** Refficks moves no money: a payout's status is\nrecorded and the merchant downloads a file for their bank, so there is\nno transfer for an endpoint to\ntrigger. Read `status: paid` as *the merchant recorded that they sent\nit*.\n","security":[{"apiKeyAuth":[]}],"parameters":[{"$ref":"#/components/parameters/Page"},{"$ref":"#/components/parameters/PerPage"},{"name":"partner_id","in":"query","schema":{"$ref":"#/components/schemas/Ulid"}},{"name":"status","in":"query","schema":{"$ref":"#/components/schemas/PayoutStatus"}},{"name":"currency","in":"query","schema":{"type":"string","minLength":3,"maxLength":3}}],"responses":{"200":{"description":"A page of payouts.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/PaginatedEnvelope"},{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ServerPayout"}}}}]}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"405":{"description":"This API does not decide money."}}}},"/server/payouts/{payout}":{"parameters":[{"$ref":"#/components/parameters/PayoutId"}],"get":{"tags":["Payouts"],"summary":"Read one payout (management API, read only)","security":[{"apiKeyAuth":[]}],"responses":{"200":{"description":"The payout.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ServerPayout"}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"description":"This API does not decide money."}}}},"/server/payouts/{payout}/entries":{"parameters":[{"$ref":"#/components/parameters/PayoutId"}],"get":{"tags":["Payouts"],"summary":"What a payout is made of (management API, read only)","description":"The question a reconciling merchant actually has, answered without\nmaking them filter the whole ledger by a payout id and trust that they\ngot all of it.\n","security":[{"apiKeyAuth":[]}],"parameters":[{"$ref":"#/components/parameters/Page"},{"$ref":"#/components/parameters/PerPage"}],"responses":{"200":{"description":"A page of the entries this payout settled.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/PaginatedEnvelope"},{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ServerCommissionEntry"}}}}]}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}}},"components":{"securitySchemes":{"apiKeyAuth":{"type":"http","scheme":"bearer","description":"An organization API key (`rk_live_…`). Only its hash is stored; the\nplaintext is shown once at creation.\n"}},"parameters":{"Page":{"name":"page","in":"query","schema":{"type":"integer","minimum":1,"default":1}},"PartnerId":{"name":"partner","in":"path","required":true,"schema":{"$ref":"#/components/schemas/Ulid"}},"PayoutId":{"name":"payout","in":"path","required":true,"schema":{"$ref":"#/components/schemas/Ulid"}},"PerPage":{"name":"per_page","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":25}},"ProgramId":{"name":"program","in":"path","required":true,"schema":{"$ref":"#/components/schemas/Ulid"}}},"schemas":{"CommissionEntryStatus":{"type":"string","enum":["pending","approved","rejected","reversed","paid"]},"CommissionEntryType":{"type":"string","enum":["earned","fee","bonus","adjustment","reversal","payout","opening","returned","owed_again","recruiter_share"]},"Error":{"type":"object","properties":{"error":{"type":"object","required":["code","message","request_id"],"properties":{"code":{"type":"string","enum":["VALIDATION_ERROR","UNAUTHENTICATED","FORBIDDEN","NOT_FOUND","CONFLICT","IDEMPOTENCY_CONFLICT","RATE_LIMITED","ENTITLEMENT_EXCEEDED","INTEGRATION_ERROR","SERVER_ERROR","MAINTENANCE","NOT_A_PARTNER_HERE","BILLING_READ_ONLY","ACCOUNT_CLOSED","PORTAL_PREVIEW_ENDED","SSO_REQUIRED","TRACKING_PAUSED"]},"message":{"type":"string"},"details":{"type":"object","description":"For `VALIDATION_ERROR`, each field's messages as a list. A few\nother codes carry facts of their own here:\n`PORTAL_PREVIEW_ENDED` names the `partner_id` whose portal the\npreview showed.\n","additionalProperties":true},"request_id":{"type":"string"}}}}},"EventInput":{"type":"object","required":["event_type","occurred_at"],"properties":{"event_type":{"description":"What a merchant's own server may report.\n\nThe line is what the caller is in a position to know. Your system\nknows what happened in your business. It is not in a position to\nreport what Refficks decided, so the conversion the attribution\nengine writes, and the lead, deal-registration and opportunity\nlifecycle Refficks records when somebody acts on a screen, are\nrefused here with a message naming what to send instead.\n\nThis list is generated from `EventType::ingestable()` and asserted\nagainst it by the test suite, so it cannot drift from what the\nendpoint accepts.\n","type":"string","enum":["tracking.visit","tracking.click","customer.created","customer.updated","lead.created","trial.started","subscription.created","subscription.updated","subscription.cancelled","invoice.created","invoice.paid","invoice.payment_failed","order.created","order.refunded","payment.refunded","payment.disputed","payment.dispute_closed","credit_note.issued"]},"external_id":{"type":"string","examples":["sub_123"]},"occurred_at":{"type":"string","format":"date-time"},"customer":{"type":"object","properties":{"external_id":{"type":"string"},"email":{"type":"string","format":"email"},"name":{"type":"string"},"country":{"type":"string","minLength":2,"maxLength":2}}},"properties":{"type":"object","description":"Normalized payload. Monetary values are integer minor units with an\nexplicit currency.\n","properties":{"amount":{"type":"integer","description":"What the customer paid.","examples":[13200]},"net_amount":{"type":"integer","description":"What you keep of it, after tax and shipping. **This is what a commission is paid on**, and what revenue reporting reads.\n\nOmit it and both fall back to `amount`. Refficks never derives a net from a gross (that would mean inventing your tax position with a partner's pay as the output), so a sale sent without this pays commission on the full amount, tax included.\n\nNever larger than `amount`; a larger value is clamped.\n","examples":[10000]},"currency":{"type":"string","examples":["USD"]},"plan":{"type":"string"},"subscription":{"type":"string","description":"#146. Which subscription this event is about: send it on an invoice for a subscription. `subscription_id` is accepted as the same thing. A `subscription.*` or `trial.started` event without it is taken to be about its own `external_id`.\n","examples":["sub_123"]},"interval":{"type":"string","enum":["day","week","month","year"],"description":"On a subscription event, how often it is billed. Without it there is no figure a month."},"interval_count":{"type":"integer","minimum":1,"description":"Billed every this many intervals: 3 with `month` is quarterly."},"quantity":{"type":"integer","minimum":1,"description":"On a subscription event, what the per-unit `amount` is multiplied by."},"status":{"type":"string","description":"On a subscription event, Stripe's word for where it stands: trialing, active, past_due, unpaid, paused, canceled."},"billing_reason":{"type":"string","description":"On an invoice, `subscription_update` marks a mid-period change of plan rather than a renewal."},"trial_ends_at":{"type":"string","format":"date-time","description":"On `trial.started`, when the trial ends."},"refunded_external_id":{"type":"string","description":"On a refund, dispute or credit note, the `external_id` of the sale it\nundoes. Send it whenever you know it: a refund that names its sale\nreverses exactly that sale, and if the sale has not arrived yet it\nwaits for it rather than reversing a different one. Left out, the\nrefund reverses the customer's most recent commission, which is a\nguess.\n"},"charge":{"type":"string","description":"On a refund or dispute, the payment it was collected on, matched against a sale recorded under that charge."},"refunded_to_date":{"type":"integer","description":"On a refund, everything refunded on the sale so far, where the reporter keeps a running total (Stripe does). When present, the reversal brings what has been taken back up to this total's share of the sale, so a repeated report takes nothing twice. Otherwise `amount` is read as this refund's own amount: a partial refund takes back its share of the commission (25 September), and refunds together never take back more than was earned.\n"},"refunded_of":{"type":"integer","description":"With `refunded_to_date`, what that running total is out of, when it is not the sale's own amount: a charge with postage on it whose sale was measured on its goods. The reversal then takes `refunded_to_date / refunded_of` of the commission, whatever the sale's amount or currency. Nought or less is read as absent.\n"}},"additionalProperties":true}}},"Money":{"type":"object","description":"Minor units plus an ISO-4217 currency. Never a float.","properties":{"amount":{"type":"integer","examples":[29900]},"currency":{"type":"string","minLength":3,"maxLength":3,"examples":["USD"]}}},"PaginatedEnvelope":{"type":"object","properties":{"data":{"type":"array","items":{}},"meta":{"type":"object","properties":{"total":{"type":"integer"},"per_page":{"type":"integer"},"current_page":{"type":"integer"},"last_page":{"type":"integer"}}},"links":{"type":"object","properties":{"next":{"type":["string","null"]},"prev":{"type":["string","null"]}}}}},"PartnerLifecycleStage":{"type":"string","enum":["applicant","approved","onboarded","active","growing","vip","dormant","at_risk","rejected"]},"PartnerRole":{"type":"string","enum":["affiliate","creator","ambassador","referrer","agency","reseller","customer","consultant"]},"PartnerStatus":{"type":"string","enum":["active","inactive","suspended","archived"]},"PayoutStatus":{"type":"string","enum":["draft","pending_approval","approved","processing","paid","failed","returned","cancelled"]},"ProgramModule":{"type":"string","enum":["affiliate","referral","creator","ambassador","agency","reseller"]},"ProgramPartnerStatus":{"type":"string","enum":["pending","approved","rejected","suspended","left"]},"ProgramStatus":{"type":"string","enum":["draft","active","paused","archived"]},"ServerCommissionEntry":{"type":"object","description":"One line of a merchant's ledger. **Read only.**\n\nAn entry carries its own arithmetic: `amount`, the `base_amount` it was\nworked out from, and the `rate`. A merchant reconciling should be able\nto see that £14.25 is ten per cent of £142.50 rather than take the\nfigure on trust.\n\nA reversal is a **new entry**, never an edit: a refund writes a negative\nrow whose `parent_entry_id` points at the original, and both stay.\n","required":["object","id","partner_id","program_id","commission_plan_id","event_id","customer_id","parent_entry_id","type","status","currency","amount","base_amount","rate","description","plan_name","payout_id","occurred_at","approved_at","paid_at","created_at"],"properties":{"object":{"type":"string","const":"commission_entry"},"id":{"$ref":"#/components/schemas/Ulid"},"partner_id":{"$ref":"#/components/schemas/Ulid"},"program_id":{"allOf":[{"$ref":"#/components/schemas/Ulid"}],"nullable":true},"commission_plan_id":{"allOf":[{"$ref":"#/components/schemas/Ulid"}],"nullable":true},"event_id":{"allOf":[{"$ref":"#/components/schemas/Ulid"}],"nullable":true,"description":"The event this was worked out from, which ties our ledger to the merchant's own system."},"customer_id":{"allOf":[{"$ref":"#/components/schemas/Ulid"}],"nullable":true},"parent_entry_id":{"allOf":[{"$ref":"#/components/schemas/Ulid"}],"nullable":true,"description":"The entry this one undoes, for a reversal."},"type":{"$ref":"#/components/schemas/CommissionEntryType"},"status":{"$ref":"#/components/schemas/CommissionEntryStatus"},"currency":{"type":"string","minLength":3,"maxLength":3,"description":"Governs both amounts. Entries in different currencies are never summed."},"amount":{"type":"integer","description":"Minor units. Negative on a reversal."},"base_amount":{"type":"integer","nullable":true,"description":"What the rate was applied to. Null where the entry is not a percentage of anything."},"rate":{"type":"number","nullable":true,"description":"A percentage (ten, not 0.1). Null alongside a null `base_amount`."},"description":{"type":"string","nullable":true},"plan_name":{"type":"string","nullable":true,"description":"The plan name recorded at the time. This is a ledger; it says what it said."},"payout_id":{"allOf":[{"$ref":"#/components/schemas/Ulid"}],"nullable":true},"occurred_at":{"type":"string","format":"date-time","description":"Never null. An entry that did not know when it happened could not\nbe placed in a period, so the column does not allow one.\n"},"approved_at":{"type":"string","format":"date-time","nullable":true},"paid_at":{"type":"string","format":"date-time","nullable":true},"created_at":{"type":"string","format":"date-time","nullable":true}}},"ServerCommissionPlan":{"type":"object","description":"What a programme pays. **Read only**: a key may set no rate, so a\n`POST` or `PATCH` here is a 405.\n\n`rules` is published verbatim because the rules *are* the plan: a\npayload saying `type: tiered` and declining to say what the tiers were\nwould answer a question nobody asked. The shape inside it belongs to\nthe calculation engine, so changing that shape changes this contract.\n","required":["object","id","program_id","name","description","type","currency","recurring","duration_months","priority","status","rules","created_at","updated_at"],"properties":{"object":{"type":"string","const":"commission_plan"},"id":{"$ref":"#/components/schemas/Ulid"},"program_id":{"$ref":"#/components/schemas/Ulid"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"type":{"type":"string","enum":["percentage","fixed","tiered","hybrid"]},"currency":{"type":"string","minLength":3,"maxLength":3},"recurring":{"type":"boolean"},"duration_months":{"type":"integer","nullable":true,"description":"Null means for as long as the subscription lasts, a different claim from a number."},"auto_approve_after_days":{"type":"integer","nullable":true,"description":"Days after a sale its commission approves itself, or null when a person approves each one. Read-only: setting it is approving money in advance, which only an admin does, in the dashboard.\n"},"priority":{"type":"integer","description":"Which plan wins when several could apply. Lower goes first."},"status":{"type":"string"},"rules":{"type":"object","additionalProperties":true},"created_at":{"type":"string","format":"date-time","nullable":true},"updated_at":{"type":"string","format":"date-time","nullable":true}}},"ServerCouponCode":{"type":"object","description":"The code a partner's audience types at the checkout. This is the\nRefficks record, not a discount in anybody's payment provider.\n","required":["object","id","program_id","partner_id","code","provider","attribution_priority","status","max_uses","uses_count","expires_at","minimum_amount","minimum_amount_currency","first_time_transaction","placements","created_at","updated_at"],"properties":{"object":{"type":"string","const":"coupon_code"},"id":{"$ref":"#/components/schemas/Ulid"},"program_id":{"$ref":"#/components/schemas/Ulid"},"partner_id":{"$ref":"#/components/schemas/Ulid"},"code":{"type":"string"},"provider":{"type":"string","nullable":true},"attribution_priority":{"type":"integer","description":"Which code wins when a basket carries more than one."},"status":{"$ref":"#/components/schemas/TrackingStatus"},"max_uses":{"type":"integer","nullable":true},"uses_count":{"type":"integer","description":"Refficks' count of redemptions. Never writable."},"expires_at":{"type":"string","format":"date-time","nullable":true},"minimum_amount":{"type":"integer","nullable":true,"description":"Minor units. Null means the programme's default applies, **not**\n\"no minimum\". A client reading null as zero is wrong about every\ncode the merchant never overrode.\n"},"minimum_amount_currency":{"type":"string","minLength":3,"maxLength":3,"nullable":true},"first_time_transaction":{"type":"boolean","nullable":true},"placements":{"type":"array","items":{"$ref":"#/components/schemas/ServerCouponPlacement"},"description":"Per connected account, because a merchant may have several and no\nsingle boolean could honestly answer for both. Empty is the\nordinary case.\n"},"created_at":{"type":"string","format":"date-time","nullable":true},"updated_at":{"type":"string","format":"date-time","nullable":true}}},"ServerCouponPlacement":{"type":"object","description":"This code, as it exists in one connected payment account. Refficks\nholds no credential, only the non-secret `acct_…` id.\n","required":["object","provider","integration_id","account_id","external_promotion_id"],"properties":{"object":{"type":"string","const":"coupon_placement"},"provider":{"type":"string","nullable":true},"integration_id":{"allOf":[{"$ref":"#/components/schemas/Ulid"}],"nullable":true},"account_id":{"type":"string","nullable":true,"description":"The account the code is in, as recorded when it was placed,\nstill named after that account is disconnected, when the\ncode is paused rather than gone.\n"},"external_promotion_id":{"type":"string","nullable":true}}},"ServerOwnRate":{"type":"object","description":"A partner's own figure on one rule. What the rule covers (its\nconditions) is the rule's; only the figure is the partner's.\n","required":["commission_plan_id","type","value","discounted_value","currency","applies","not_applying_because","reason","set_at"],"properties":{"commission_plan_id":{"$ref":"#/components/schemas/Ulid"},"type":{"type":"string","enum":["percentage","fixed"]},"value":{"type":"number","description":"A percentage, or for `fixed` minor units of `currency`, as the rule's own `rules.action.value` is written."},"discounted_value":{"type":"number","nullable":true,"description":"Their figure on a sale with a discount code, when they were given one. Null keeps the rule's."},"currency":{"type":"string","nullable":true,"description":"The rule's currency."},"applies":{"type":"boolean","description":"Whether this figure is what the rule pays them today."},"not_applying_because":{"type":"string","nullable":true,"enum":["rule_archived","rule_pays_differently",null],"description":"`rule_archived`: the rule pays nobody; bringing it back pays this\nfigure again. `rule_pays_differently`: the rule changed how it pays\nsince the figure was set, so the rule's own figure applies.\n"},"reason":{"type":"string","nullable":true,"description":"The merchant's note on why."},"set_at":{"type":"string","format":"date-time","nullable":true}}},"ServerPartner":{"type":"object","description":"A partner as the management API publishes one.\n\nDistinct from `Partner`, which is the dashboard's shape and follows its\nscreens. **Every property below is always present**; absent data is\n`null`, never missing. Within v1 properties are added and never removed\nor repurposed.\n\nThe scores (`score`, `potential_score`, `risk_score`) are deliberately\nabsent: they are recomputed on a schedule we change, against a scale\nnobody outside has agreed to, and automating against one would be\nautomating against an implementation detail.\n","required":["object","id","status","lifecycle_stage","email","first_name","last_name","company_name","phone","country","timezone","website","currency","lifetime_revenue_amount","lifetime_revenue_currency","lifetime_revenue_by_currency","lifetime_commission_amount","lifetime_commission_currency","lifetime_commission_by_currency","roles","tags","first_click_at","first_conversion_at","last_activity_at","approved_at","forgotten_at","created_at","updated_at"],"properties":{"object":{"type":"string","const":"partner"},"id":{"$ref":"#/components/schemas/Ulid"},"status":{"$ref":"#/components/schemas/PartnerStatus"},"lifecycle_stage":{"$ref":"#/components/schemas/PartnerLifecycleStage"},"email":{"type":"string","format":"email"},"first_name":{"type":"string","nullable":true},"last_name":{"type":"string","nullable":true},"company_name":{"type":"string","nullable":true},"phone":{"type":"string","nullable":true},"country":{"type":"string","minLength":2,"maxLength":2,"nullable":true,"description":"ISO 3166-1 alpha-2, stored upper-case."},"timezone":{"type":"string","nullable":true},"website":{"type":"string","format":"uri","nullable":true},"currency":{"type":"string","minLength":3,"maxLength":3,"description":"ISO 4217. The currency both amounts below are in. On the object\nrather than implied, because a figure without its currency is a\nfigure somebody will add to another one.\n","examples":["GBP"]},"lifetime_revenue_amount":{"type":"integer","description":"Minor units (pence, cents) as an integer. Never a decimal string.\nIn `lifetime_revenue_currency`, the merchant's currency, not\n`currency`: sales in other currencies are in\n`lifetime_revenue_by_currency` and never added to this. It used to\nbe every currency added together.\n"},"lifetime_revenue_currency":{"type":"string","description":"The merchant's currency, which `lifetime_revenue_amount` is in."},"lifetime_revenue_by_currency":{"type":"array","description":"Every currency the partner has sold in, the merchant's first (always present, at 0 if nothing was sold in it), then the largest.","items":{"$ref":"#/components/schemas/Money"}},"lifetime_commission_amount":{"type":"integer","description":"What the partner has earned, in the merchant's currency (`lifetime_commission_currency`), read from the ledger: every entry that was not rejected, so a reversal nets its sale away. Commission in other currencies is in `lifetime_commission_by_currency` and never added to this. It used to be a running sum of every currency."},"lifetime_commission_currency":{"type":"string","description":"The merchant's currency, which `lifetime_commission_amount` is in."},"lifetime_commission_by_currency":{"type":"array","description":"Every currency the partner has earned in, the merchant's first (always present, at 0 if nothing was earned in it), then the largest.","items":{"$ref":"#/components/schemas/Money"}},"roles":{"type":"array","items":{"$ref":"#/components/schemas/PartnerRole"},"description":"Empty rather than absent for a partner with none."},"tags":{"type":"array","items":{"type":"string"}},"first_click_at":{"type":"string","format":"date-time","nullable":true},"first_conversion_at":{"type":"string","format":"date-time","nullable":true},"last_activity_at":{"type":"string","format":"date-time","nullable":true},"approved_at":{"type":"string","format":"date-time","nullable":true},"forgotten_at":{"type":"string","format":"date-time","nullable":true,"description":"When this record was erased, or null. Published because every other\nfield on a forgotten partner is blanked, so a client left to infer\nit would be inferring from an absence, and a server syncing records\nneeds to read *this person asked to be forgotten* rather than *this\nperson is called something odd now*.\n"},"created_at":{"type":"string","format":"date-time","nullable":true},"updated_at":{"type":"string","format":"date-time","nullable":true}}},"ServerPayout":{"type":"object","description":"A payout. **Read only**, and Refficks moves no money:\n`status: paid` means the merchant recorded that they sent it.\n\n**`payment_details` is not here and never will be.** Payout details are\nencrypted at rest and the stored value is never returned to anybody.\n`payment_hint` is the mask, and is all this surface says about where\nmoney went.\n","required":["object","id","partner_id","status","currency","amount","entries_count","payment_method","payment_hint","reference","void_reason","external_payout_id","period_start","period_end","approved_at","paid_at","created_at","updated_at"],"properties":{"object":{"type":"string","const":"payout"},"id":{"$ref":"#/components/schemas/Ulid"},"partner_id":{"$ref":"#/components/schemas/Ulid"},"status":{"$ref":"#/components/schemas/PayoutStatus"},"currency":{"type":"string","minLength":3,"maxLength":3},"amount":{"type":"integer","description":"Minor units."},"entries_count":{"type":"integer"},"payment_method":{"type":"string","nullable":true},"payment_hint":{"type":"string","nullable":true,"description":"The masked account: enough to recognise it on a statement, useless\nto anybody who obtains this payload. The real value leaves Refficks\nthrough the audited bank export alone.\n"},"reference":{"type":"string","nullable":true},"void_reason":{"type":"string","nullable":true,"description":"Why the merchant called this payout off, in their own words.\nReadable here because the caller on this surface *is* the merchant.\nIt is merchant-private: the partner it is about never receives it,\non this surface or any other. Earlier versions of the API called it\n`notes`.\n"},"external_payout_id":{"type":"string","nullable":true,"description":"The merchant's note of a transfer they made elsewhere. Refficks did not put it there."},"period_start":{"type":"string","format":"date","nullable":true},"period_end":{"type":"string","format":"date","nullable":true},"approved_at":{"type":"string","format":"date-time","nullable":true},"paid_at":{"type":"string","format":"date-time","nullable":true},"created_at":{"type":"string","format":"date-time","nullable":true},"updated_at":{"type":"string","format":"date-time","nullable":true}}},"ServerProgram":{"type":"object","description":"A programme as the management API publishes one.\nEvery property is always present; absent data is `null`.\n","required":["object","id","module","name","slug","description","status","default_currency","attribution_window_days","auto_approve_partners","terms_url","default_destination_url","created_at","updated_at"],"properties":{"object":{"type":"string","const":"program"},"id":{"$ref":"#/components/schemas/Ulid"},"module":{"$ref":"#/components/schemas/ProgramModule"},"name":{"type":"string"},"slug":{"type":"string","description":"The last part of this programme's public address."},"description":{"type":"string","nullable":true},"status":{"$ref":"#/components/schemas/ProgramStatus"},"default_currency":{"type":"string","minLength":3,"maxLength":3},"attribution_window_days":{"type":"integer","description":"How long a click keeps earning, what other platforms call the\ncookie duration. Captured at click time, so changing it never\nre-decides a conversion that already happened.\n"},"auto_approve_partners":{"type":"boolean"},"terms_url":{"type":"string","format":"uri","nullable":true},"default_destination_url":{"type":"string","format":"uri","nullable":true,"description":"Where links under this programme land when the link does not say.\nWithout it, every link you mint must carry its own.\n"},"created_at":{"type":"string","format":"date-time","nullable":true},"updated_at":{"type":"string","format":"date-time","nullable":true}}},"ServerProgramMembership":{"type":"object","description":"One partner's place in one programme: where the terms live.\n`custom_commission_plan_id` is readable and never writable.\n","required":["object","id","program_id","partner_id","status","tier","custom_commission_plan_id","own_rates","joined_at","approved_at","created_at","updated_at"],"properties":{"object":{"type":"string","const":"program_membership"},"id":{"$ref":"#/components/schemas/Ulid"},"program_id":{"$ref":"#/components/schemas/Ulid"},"partner_id":{"$ref":"#/components/schemas/Ulid"},"status":{"$ref":"#/components/schemas/ProgramPartnerStatus"},"tier":{"type":"string","nullable":true},"custom_commission_plan_id":{"allOf":[{"$ref":"#/components/schemas/Ulid"}],"nullable":true,"description":"A plan of this partner's own that replaced every rule, from before\na partner could have a figure per rule. Saving figures per rule on\nthe dashboard retires it. What it pays belongs to the plan.\n"},"own_rates":{"type":"array","description":"This partner's own figures on the programme's rules, in\nforce. Read only: a figure is set on the partner's card in the\ndashboard.\n","items":{"$ref":"#/components/schemas/ServerOwnRate"}},"joined_at":{"type":"string","format":"date-time","nullable":true},"approved_at":{"type":"string","format":"date-time","nullable":true},"created_at":{"type":"string","format":"date-time","nullable":true},"updated_at":{"type":"string","format":"date-time","nullable":true}}},"ServerTrackingLink":{"type":"object","description":"A partner's link. `url` and `destination_url` are **derived, not\nstored**: a merchant who changes their link base expects every\nexisting link to move.\n","required":["object","id","program_id","partner_id","code","name","url","destination_url","attribution_window_days","attribution_window_source","clicks_count","status","created_at","updated_at"],"properties":{"object":{"type":"string","const":"tracking_link"},"id":{"$ref":"#/components/schemas/Ulid"},"program_id":{"$ref":"#/components/schemas/Ulid"},"partner_id":{"$ref":"#/components/schemas/Ulid"},"code":{"type":"string","description":"Printed wherever the partner shared it, and therefore never changeable."},"name":{"type":"string","nullable":true},"url":{"type":"string","format":"uri","description":"The form a partner shares."},"destination_url":{"type":"string","format":"uri","description":"Where a click lands."},"attribution_window_days":{"type":"integer"},"attribution_window_source":{"type":"string","enum":["link","program"],"description":"Which setting decided the window. The number alone cannot be acted\non: reading 30 does not say whether editing the programme moves it.\n"},"clicks_count":{"type":"integer"},"status":{"$ref":"#/components/schemas/TrackingStatus"},"created_at":{"type":"string","format":"date-time","nullable":true},"updated_at":{"type":"string","format":"date-time","nullable":true}}},"TrackingStatus":{"type":"string","enum":["active","paused","archived"]},"Ulid":{"type":"string","pattern":"^[0-9A-HJKMNP-TV-Z]{26}$","examples":["01JQ8Z9M4K7X2N5P6Q3R8S1T4V"]}},"responses":{"EntitlementExceeded":{"description":"The organization's plan limit for this resource has been reached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Forbidden":{"description":"Authenticated, but not permitted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"IdempotencyConflict":{"description":"The idempotency key was reused with a different payload.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"No such resource, including a resource that belongs to another tenant.\nCross-tenant access returns 404 rather than 403, because a 403 confirms\nthe resource exists.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Unauthenticated":{"description":"Missing or invalid credential.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"ValidationError":{"description":"The request failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}