{"openapi":"3.1.0","info":{"title":"TPP Public API","version":"1.16.0","description":"**Reward your players. Anywhere.** Plug your CRM, loyalty\nengine or partner integration into TPP and:\n\n- 🎁 **Onboard players** the moment they sign up on your casino.\n- 🪙 **Run their coin wallet** — credit bonuses, debit redemptions, audit every movement.\n- 📦 **Place and track orders** from `pending` to `delivered`, with carrier + tracking.\n- 🛍️ **Browse the live catalog** scoped to your sub-brand and country.\n- ↩️ **Cancel, query, repeat** — every mutation idempotent, every response auditable.\n\n**Quick start.** Get a `tpp_live_*` (or `tpp_test_*`) bearer\ntoken from **Sub-brand → API keys** in the backoffice, paste\nit into the auth panel on the right, and try any endpoint\nfrom this page. Wire format is `snake_case` JSON; money is\ninteger minor units with a separate `currency`; IDs are\nUUIDv7 strings.\n\nNew here? Open the **Integration** page in the sidebar for\nthe full picture of how a partner integration flows end to end."},"servers":[{"url":"https://demo-api.thepowerplugin.com","description":"demo-2"}],"tags":[{"name":"Integration","description":"A partner integration is **five HTTP calls** in a loop — onboard\na player, credit their wallet, browse the catalog, reserve an\norder, track it to the door.\n\nThe full walkthrough — flow diagram + a paragraph per endpoint +\nquick-start + partner guarantees — lives on a dedicated,\nfull-width page:\n\n### 👉 [Open the Integration Guide](/api/v1/guide)\n\nPrefer to dive straight into the endpoints? Every operation in\nthe sidebar has a live \"Try it\" console — paste your bearer\ntoken into the auth panel and go."},{"name":"System","description":"Operational endpoints for your integration's plumbing. Verify your key works with `/health`, download the machine-readable contract from `/openapi.json`, or land here at `/docs` to share these pages with a colleague."},{"name":"Customers","description":"Your players, on TPP. Register them the moment they sign up on your casino, keep their profile in sync as they update it, and run their virtual-coin wallet — credits, debits, full ledger history — directly from your loyalty backend. Each customer is scoped to one sub-brand, so multi-brand operators get clean isolation out of the box."},{"name":"Stores","description":"The storefronts where your players actually shop. Stores are configured visually in the backoffice (theme, layout, payment mode, languages); the API exposes them so your integration can deep-link players to the right URL, attribute redemptions to the right store, and surface live data in companion apps."},{"name":"Catalogs","description":"Curated product selections you tailor for each sub-brand. Catalogs can be brand-wide (with explicit acceptance per sub-brand) or sub-brand-exclusive. The API only returns the catalogs your key can actually use — pending or rejected proposals stay invisible, so your integrators never confuse what's offered with what's coming soon."},{"name":"Categories"},{"name":"Brands"},{"name":"Products","description":"Everything redeemable, from the latest console to a branded hoodie. Filter by category or catalog, search by name, fetch full variant detail with SKUs and images. Status, exclusivity, and country availability are resolved automatically — what the API returns IS what your player can redeem right now."},{"name":"Orders","description":"Where redemptions become packages. Reserve an order, track it through `pending → processing → shipped → delivered`, query carrier and tracking details for customer-facing UIs, or cancel before it ships. Billing-aware: prepaid brands without saldo see `billing_status: \"blocked_balance\"` so your operators can resolve before fulfilment proceeds."},{"name":"Store sessions"},{"name":"Engine"}],"security":[{"BearerApiKey":[]}],"paths":{"/api/v1/health":{"get":{"operationId":"getHealth","summary":"Liveness probe and key sanity check","description":"Verifies the calling key authenticates, has at least the `customers:read` scope, and counts against its rate-limit bucket. Returns the environment and sub-brand id encoded in the key so clients can confirm they are talking to the right tenant/mode.","tags":["System"],"security":[{"BearerApiKey":["customers:read"]}],"responses":{"200":{"description":"OK","headers":{"x-request-id":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Echo of the per-request id (`req_<32 hex>`)."},"x-ratelimit-limit":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Per-minute request cap for this key's tier."},"x-ratelimit-remaining":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Remaining requests in the current 60-second window."},"x-ratelimit-reset":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Unix timestamp (seconds) when the current window resets."}},"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"status":{"type":"string","const":"ok"},"environment":{"type":"string","enum":["live","test"]},"sub_brand_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"api_version":{"type":"string","const":"v1"},"server_time":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"}},"required":["status","environment","sub_brand_id","api_version","server_time"],"additionalProperties":false},"page":{"$ref":"#/components/schemas/Page"}},"required":["data"],"additionalProperties":false}}}},"400":{"description":"Invalid request body or parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, malformed, or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API key lacks the required scope or allowlist gate","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Resource not found or not visible to this key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflict — idempotency key reuse or unique violation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error — check `request_id` and contact support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/customers":{"get":{"operationId":"listCustomers","summary":"List customers","description":"Returns the calling key's sub-brand customers in reverse\nchronological order. Pagination is cursor-based — call again with\n`?cursor=<page.next_cursor>` until `page.has_more` is false.\n\nAll filters combine with AND. Filter by status, segment, country,\ncreation/registration date windows, coin-balance range and\nlifetime order-count range — the same targeting the bulk-coins\nendpoint uses.","tags":["Customers"],"security":[{"BearerApiKey":["customers:read"]}],"parameters":[{"in":"query","name":"q","schema":{"description":"Substring match across `first_name`, `last_name`, `email`, `external_id`. Case-insensitive.","type":"string","minLength":1,"maxLength":200},"description":"Substring match across `first_name`, `last_name`, `email`, `external_id`. Case-insensitive."},{"in":"query","name":"status","schema":{"description":"Filter by moderation status.","type":"string","enum":["active","watch","restricted","banned"]},"description":"Filter by moderation status."},{"in":"query","name":"segment_id","schema":{"description":"Exact match on the casino-side segment/cohort tag.","type":"string","minLength":1,"maxLength":36},"description":"Exact match on the casino-side segment/cohort tag."},{"in":"query","name":"country","schema":{"description":"ISO-3166-1 alpha-2 country.","type":"string","minLength":2,"maxLength":2},"description":"ISO-3166-1 alpha-2 country."},{"in":"query","name":"created_after","schema":{"description":"Only customers created at/after this ISO-8601 UTC instant.","type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"description":"Only customers created at/after this ISO-8601 UTC instant."},{"in":"query","name":"created_before","schema":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"}},{"in":"query","name":"registered_after","schema":{"description":"Only customers whose casino `platform_registered_at` is at/after this instant.","type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"description":"Only customers whose casino `platform_registered_at` is at/after this instant."},{"in":"query","name":"registered_before","schema":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"}},{"in":"query","name":"min_coins","schema":{"description":"Only customers whose coin balance is >= this value (0 default).","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"description":"Only customers whose coin balance is >= this value (0 default)."},{"in":"query","name":"max_coins","schema":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},{"in":"query","name":"min_orders","schema":{"description":"Only customers with at least this many lifetime orders.","type":"integer","minimum":0,"maximum":9007199254740991},"description":"Only customers with at least this many lifetime orders."},{"in":"query","name":"max_orders","schema":{"type":"integer","minimum":0,"maximum":9007199254740991}},{"in":"query","name":"limit","schema":{"description":"Default 25, max 100.","type":"integer","minimum":1,"maximum":100},"description":"Default 25, max 100."},{"in":"query","name":"cursor","schema":{"description":"Opaque cursor returned by the previous page's `page.next_cursor`. Omit on the first page.","type":"string"},"description":"Opaque cursor returned by the previous page's `page.next_cursor`. Omit on the first page."}],"responses":{"200":{"description":"OK","headers":{"x-request-id":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Echo of the per-request id (`req_<32 hex>`)."},"x-ratelimit-limit":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Per-minute request cap for this key's tier."},"x-ratelimit-remaining":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Remaining requests in the current 60-second window."},"x-ratelimit-reset":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Unix timestamp (seconds) when the current window resets."}},"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Customer"}},"page":{"$ref":"#/components/schemas/Page"}},"required":["data","page"],"additionalProperties":false}}}},"400":{"description":"Invalid request body or parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, malformed, or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API key lacks the required scope or allowlist gate","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Resource not found or not visible to this key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflict — idempotency key reuse or unique violation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error — check `request_id` and contact support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"createCustomer","summary":"Create a customer","description":"Creates a customer in the calling key's sub-brand. The\n`Idempotency-Key` header is **required** — retries with the\nsame key + same body return the cached response; retries with\nthe same key + a different body return 409 `idempotency_conflict`.\n\nIf `external_id` collides with an existing customer in this\nsub-brand, returns 409 `conflict`.","tags":["Customers"],"security":[{"BearerApiKey":["customers:create"]}],"parameters":[{"in":"header","name":"Idempotency-Key","schema":{"type":"string","minLength":1,"maxLength":64,"description":"Opaque ASCII string (max 64 chars) used to deduplicate replays for 24h.","example":"01HXYZABC-create-customer"},"required":true,"description":"Opaque ASCII string (max 64 chars) used to deduplicate replays for 24h."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"external_id":{"description":"Caller-supplied identifier. Unique per sub-brand — duplicates return 409 conflict.","anyOf":[{"type":"string","maxLength":120},{"type":"null"}]},"segment_id":{"anyOf":[{"type":"string","maxLength":36},{"type":"null"}]},"first_name":{"type":"string","minLength":1,"maxLength":120},"last_name":{"type":"string","minLength":1,"maxLength":120},"nickname":{"anyOf":[{"type":"string","maxLength":120},{"type":"null"}]},"email":{"anyOf":[{"type":"string","maxLength":255,"format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},{"type":"null"}]},"phone":{"anyOf":[{"type":"string","maxLength":40},{"type":"null"}]},"address_line":{"anyOf":[{"type":"string","maxLength":240},{"type":"null"}]},"postal_code":{"anyOf":[{"type":"string","maxLength":40},{"type":"null"}]},"zone":{"anyOf":[{"type":"string","maxLength":120},{"type":"null"}]},"locality":{"anyOf":[{"type":"string","maxLength":120},{"type":"null"}]},"city":{"anyOf":[{"type":"string","maxLength":120},{"type":"null"}]},"state":{"anyOf":[{"type":"string","maxLength":120},{"type":"null"}]},"country":{"anyOf":[{"type":"string","minLength":2,"maxLength":2},{"type":"null"}]},"preferred_lang":{"anyOf":[{"type":"string","enum":["en","es","pt","it","fr"]},{"type":"null"}]},"platform_registered_at":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},{"type":"null"}]},"status":{"type":"string","enum":["active","watch","restricted","banned"]}},"required":["first_name","last_name"]}}}},"responses":{"201":{"description":"Created","headers":{"x-request-id":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Echo of the per-request id (`req_<32 hex>`)."},"x-ratelimit-limit":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Per-minute request cap for this key's tier."},"x-ratelimit-remaining":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Remaining requests in the current 60-second window."},"x-ratelimit-reset":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Unix timestamp (seconds) when the current window resets."}},"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Customer"},"page":{"$ref":"#/components/schemas/Page"}},"required":["data"],"additionalProperties":false}}}},"400":{"description":"Invalid request body or parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, malformed, or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API key lacks the required scope or allowlist gate","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Resource not found or not visible to this key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflict — idempotency key reuse or unique violation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error — check `request_id` and contact support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/customers/{id}":{"get":{"operationId":"getCustomer","summary":"Fetch a customer by id","description":"Returns the customer if it belongs to the calling key's sub-brand.\n\n404 is intentionally returned for both unknown ids and ids in\nother sub-brands — distinguishing them would leak existence.","tags":["Customers"],"security":[{"BearerApiKey":["customers:read"]}],"responses":{"200":{"description":"OK","headers":{"x-request-id":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Echo of the per-request id (`req_<32 hex>`)."},"x-ratelimit-limit":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Per-minute request cap for this key's tier."},"x-ratelimit-remaining":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Remaining requests in the current 60-second window."},"x-ratelimit-reset":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Unix timestamp (seconds) when the current window resets."}},"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Customer"},"page":{"$ref":"#/components/schemas/Page"}},"required":["data"],"additionalProperties":false}}}},"400":{"description":"Invalid request body or parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, malformed, or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API key lacks the required scope or allowlist gate","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Resource not found or not visible to this key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflict — idempotency key reuse or unique violation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error — check `request_id` and contact support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"operationId":"updateCustomer","summary":"Partial-update a customer","description":"Updates the fields present in the body. Omitted fields are NOT\nchanged. To unset a nullable field, send it explicitly as `null`\n(this is PATCH semantics, NOT JSON-merge-patch).\n\n`status` transitions stamp `status_changed_at` automatically.","tags":["Customers"],"security":[{"BearerApiKey":["customers:update"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"external_id":{"anyOf":[{"type":"string","maxLength":120},{"type":"null"}]},"segment_id":{"anyOf":[{"type":"string","maxLength":36},{"type":"null"}]},"first_name":{"type":"string","minLength":1,"maxLength":120},"last_name":{"type":"string","minLength":1,"maxLength":120},"nickname":{"anyOf":[{"type":"string","maxLength":120},{"type":"null"}]},"email":{"anyOf":[{"type":"string","maxLength":255,"format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},{"type":"null"}]},"phone":{"anyOf":[{"type":"string","maxLength":40},{"type":"null"}]},"address_line":{"anyOf":[{"type":"string","maxLength":240},{"type":"null"}]},"postal_code":{"anyOf":[{"type":"string","maxLength":40},{"type":"null"}]},"zone":{"anyOf":[{"type":"string","maxLength":120},{"type":"null"}]},"locality":{"anyOf":[{"type":"string","maxLength":120},{"type":"null"}]},"city":{"anyOf":[{"type":"string","maxLength":120},{"type":"null"}]},"state":{"anyOf":[{"type":"string","maxLength":120},{"type":"null"}]},"country":{"anyOf":[{"type":"string","minLength":2,"maxLength":2},{"type":"null"}]},"preferred_lang":{"anyOf":[{"type":"string","enum":["en","es","pt","it","fr"]},{"type":"null"}]},"platform_registered_at":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},{"type":"null"}]},"status":{"type":"string","enum":["active","watch","restricted","banned"]},"status_note":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}]}}}}}},"responses":{"200":{"description":"OK","headers":{"x-request-id":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Echo of the per-request id (`req_<32 hex>`)."},"x-ratelimit-limit":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Per-minute request cap for this key's tier."},"x-ratelimit-remaining":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Remaining requests in the current 60-second window."},"x-ratelimit-reset":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Unix timestamp (seconds) when the current window resets."}},"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Customer"},"page":{"$ref":"#/components/schemas/Page"}},"required":["data"],"additionalProperties":false}}}},"400":{"description":"Invalid request body or parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, malformed, or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API key lacks the required scope or allowlist gate","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Resource not found or not visible to this key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflict — idempotency key reuse or unique violation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error — check `request_id` and contact support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"operationId":"deleteCustomer","summary":"Delete a customer","description":"Hard delete. Cascades through FKs:\n- `order_items.customer_id` is set to NULL (orders remain).\n- `customer_coin_balances` and `customer_coin_movements` are deleted.\n\nReturns `{ id, deleted: true }` on success. Idempotency is not\nrequired — DELETE is already idempotent at the HTTP level.","tags":["Customers"],"security":[{"BearerApiKey":["customers:delete"]}],"responses":{"200":{"description":"OK","headers":{"x-request-id":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Echo of the per-request id (`req_<32 hex>`)."},"x-ratelimit-limit":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Per-minute request cap for this key's tier."},"x-ratelimit-remaining":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Remaining requests in the current 60-second window."},"x-ratelimit-reset":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Unix timestamp (seconds) when the current window resets."}},"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"deleted":{"type":"boolean","const":true}},"required":["id","deleted"],"additionalProperties":false},"page":{"$ref":"#/components/schemas/Page"}},"required":["data"],"additionalProperties":false}}}},"400":{"description":"Invalid request body or parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, malformed, or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API key lacks the required scope or allowlist gate","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Resource not found or not visible to this key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflict — idempotency key reuse or unique violation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error — check `request_id` and contact support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/customers/{id}/coins":{"get":{"operationId":"getCustomerCoins","summary":"Get a customer's coin wallet snapshot","description":"Returns the cached balance plus the 10 most recent movements\nfor the customer. Convenient for an activity-feed UI that\nshows running history without a separate paginated call.\n\nReturns `balance: 0` with an empty `recent_movements` array\nwhen the customer's wallet has never been touched.\n\nFor deeper history, a paginated `/coins/movements` endpoint\nships in a future release (v1.x).","tags":["Customers"],"security":[{"BearerApiKey":["customers:read"]}],"responses":{"200":{"description":"OK","headers":{"x-request-id":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Echo of the per-request id (`req_<32 hex>`)."},"x-ratelimit-limit":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Per-minute request cap for this key's tier."},"x-ratelimit-remaining":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Remaining requests in the current 60-second window."},"x-ratelimit-reset":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Unix timestamp (seconds) when the current window resets."}},"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/CoinWallet"},"page":{"$ref":"#/components/schemas/Page"}},"required":["data"],"additionalProperties":false}}}},"400":{"description":"Invalid request body or parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, malformed, or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API key lacks the required scope or allowlist gate","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Resource not found or not visible to this key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflict — idempotency key reuse or unique violation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error — check `request_id` and contact support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"adjustCustomerCoins","summary":"Adjust a customer's coin balance","description":"Credits (positive `delta`) or debits (negative `delta`) the\ncustomer's virtual-coin wallet. Runs inside a transaction with\n`SELECT … FOR UPDATE` so concurrent debits cannot both pass the\nnon-negative balance guard.\n\n**Idempotency-Key is required.** Replaying with the same key +\nthe same body returns the cached response (the balance from the\noriginal execution, NOT a fresh read). Replaying with a different\nbody returns 409 `idempotency_conflict`.\n\nConstraints:\n- `delta` is a signed integer, non-zero, `|delta| <= 1_000_000`.\n- Debits that would leave the balance negative return 409 `conflict`\n  with `balance_before` in the error extras.","tags":["Customers"],"security":[{"BearerApiKey":["customers:coins"]}],"parameters":[{"in":"header","name":"Idempotency-Key","schema":{"type":"string","minLength":1,"maxLength":64,"description":"Opaque ASCII string (max 64 chars) used to deduplicate replays for 24h.","example":"01HXYZABC-create-customer"},"required":true,"description":"Opaque ASCII string (max 64 chars) used to deduplicate replays for 24h."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"delta":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"reason":{"type":"string","maxLength":500}},"required":["delta"]}}}},"responses":{"200":{"description":"OK","headers":{"x-request-id":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Echo of the per-request id (`req_<32 hex>`)."},"x-ratelimit-limit":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Per-minute request cap for this key's tier."},"x-ratelimit-remaining":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Remaining requests in the current 60-second window."},"x-ratelimit-reset":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Unix timestamp (seconds) when the current window resets."}},"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/CoinAdjustment"},"page":{"$ref":"#/components/schemas/Page"}},"required":["data"],"additionalProperties":false}}}},"400":{"description":"Invalid request body or parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, malformed, or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API key lacks the required scope or allowlist gate","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Resource not found or not visible to this key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflict — idempotency key reuse or unique violation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error — check `request_id` and contact support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/customers/{id}/coins/movements":{"get":{"operationId":"listCustomerCoinMovements","summary":"List a customer's full coin-movement history","description":"Returns every movement for the customer's wallet in reverse\nchronological order, cursor-paginated. Movements are append-\nonly — corrections appear as NEW rows with their own `delta`\nand `balance_after`, so the running history is reconstructable\nfrom this endpoint alone.\n\n404 if the customer doesn't exist or belongs to a different\nsub-brand.","tags":["Customers"],"security":[{"BearerApiKey":["customers:read"]}],"parameters":[{"in":"query","name":"limit","schema":{"description":"Page size — default 25, max 100. Smaller pages are cheaper; large pages save round-trips when backfilling history.","type":"integer","minimum":1,"maximum":100},"description":"Page size — default 25, max 100. Smaller pages are cheaper; large pages save round-trips when backfilling history."},{"in":"query","name":"cursor","schema":{"description":"Opaque cursor from the previous page's `page.next_cursor`. Omit on the first page.","type":"string"},"description":"Opaque cursor from the previous page's `page.next_cursor`. Omit on the first page."}],"responses":{"200":{"description":"OK","headers":{"x-request-id":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Echo of the per-request id (`req_<32 hex>`)."},"x-ratelimit-limit":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Per-minute request cap for this key's tier."},"x-ratelimit-remaining":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Remaining requests in the current 60-second window."},"x-ratelimit-reset":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Unix timestamp (seconds) when the current window resets."}},"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CoinMovement"}},"page":{"$ref":"#/components/schemas/Page"}},"required":["data","page"],"additionalProperties":false}}}},"400":{"description":"Invalid request body or parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, malformed, or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API key lacks the required scope or allowlist gate","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Resource not found or not visible to this key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflict — idempotency key reuse or unique violation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error — check `request_id` and contact support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/customers/bulk/coins":{"post":{"operationId":"bulkAdjustCustomerCoins","summary":"Bulk-adjust coins over a customer segment","description":"Credits (positive `delta`) or debits (negative `delta`) the coin\nwallet of every customer matching `filter`, scoped to the calling\nkey's sub-brand.\n\nThe filter targets almost any customer attribute: `status`,\n`segment_id`, `country`, registration/creation date windows\n(`created_after`/`before`, `registered_after`/`before`), coin\nbalance range (`min_coins`/`max_coins`) and lifetime order count\n(`min_orders`/`max_orders`), plus free-text `q`.\n\n**Idempotency-Key is required.** The whole batch is one\ntransaction, so a retry after a partial failure re-applies cleanly\n(no double-credit).\n\nConstraints:\n- A non-empty filter is required (no accidental credit-everyone).\n- A match set larger than 1000 is rejected — narrow the filter.\n- Debits that would push a wallet negative are skipped and listed\n  in `skipped`.\n- Send `dry_run: true` to preview `matched_count` + a `sample`\n  before committing.","tags":["Customers"],"security":[{"BearerApiKey":["customers:coins"]}],"parameters":[{"in":"header","name":"Idempotency-Key","schema":{"type":"string","minLength":1,"maxLength":64,"description":"Opaque ASCII string (max 64 chars) used to deduplicate replays for 24h.","example":"01HXYZABC-create-customer"},"required":true,"description":"Opaque ASCII string (max 64 chars) used to deduplicate replays for 24h."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"filter":{"type":"object","properties":{"q":{"type":"string","minLength":1,"maxLength":120},"status":{"type":"string","enum":["active","watch","restricted","banned"]},"segment_id":{"type":"string","minLength":1,"maxLength":36},"country":{"type":"string","minLength":2,"maxLength":2},"created_after":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"created_before":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"registered_after":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"registered_before":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"min_coins":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"max_coins":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"min_orders":{"type":"integer","minimum":0,"maximum":9007199254740991},"max_orders":{"type":"integer","minimum":0,"maximum":9007199254740991}}},"delta":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"reason":{"type":"string","maxLength":500},"dry_run":{"default":false,"type":"boolean"}},"required":["filter","delta"]}}}},"responses":{"200":{"description":"OK","headers":{"x-request-id":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Echo of the per-request id (`req_<32 hex>`)."},"x-ratelimit-limit":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Per-minute request cap for this key's tier."},"x-ratelimit-remaining":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Remaining requests in the current 60-second window."},"x-ratelimit-reset":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Unix timestamp (seconds) when the current window resets."}},"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/BulkCoinsResult"},"page":{"$ref":"#/components/schemas/Page"}},"required":["data"],"additionalProperties":false}}}},"400":{"description":"Invalid request body or parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, malformed, or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API key lacks the required scope or allowlist gate","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Resource not found or not visible to this key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflict — idempotency key reuse or unique violation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error — check `request_id` and contact support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/stores":{"get":{"operationId":"listStores","summary":"List storefronts","description":"Cursor-paginated list of storefronts belonging to the calling key's sub-brand. Filter by status or name/slug substring.","tags":["Stores"],"security":[{"BearerApiKey":["stores:read"]}],"parameters":[{"in":"query","name":"q","schema":{"type":"string","minLength":1,"maxLength":200}},{"in":"query","name":"status","schema":{"type":"string","enum":["draft","active","inactive"]}},{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":100}},{"in":"query","name":"cursor","schema":{"type":"string"}}],"responses":{"200":{"description":"OK","headers":{"x-request-id":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Echo of the per-request id (`req_<32 hex>`)."},"x-ratelimit-limit":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Per-minute request cap for this key's tier."},"x-ratelimit-remaining":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Remaining requests in the current 60-second window."},"x-ratelimit-reset":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Unix timestamp (seconds) when the current window resets."}},"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Store"}},"page":{"$ref":"#/components/schemas/Page"}},"required":["data","page"],"additionalProperties":false}}}},"400":{"description":"Invalid request body or parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, malformed, or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API key lacks the required scope or allowlist gate","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Resource not found or not visible to this key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflict — idempotency key reuse or unique violation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error — check `request_id` and contact support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/stores/{id}":{"get":{"operationId":"getStore","summary":"Fetch a store by id or slug","description":"The `{id}` path parameter accepts either a UUID or the store's slug.\nSlugs are globally unique so disambiguation by shape is safe.\n\nReturns 404 if the store doesn't exist OR belongs to a different sub-brand.","tags":["Stores"],"security":[{"BearerApiKey":["stores:read"]}],"responses":{"200":{"description":"OK","headers":{"x-request-id":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Echo of the per-request id (`req_<32 hex>`)."},"x-ratelimit-limit":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Per-minute request cap for this key's tier."},"x-ratelimit-remaining":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Remaining requests in the current 60-second window."},"x-ratelimit-reset":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Unix timestamp (seconds) when the current window resets."}},"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Store"},"page":{"$ref":"#/components/schemas/Page"}},"required":["data"],"additionalProperties":false}}}},"400":{"description":"Invalid request body or parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, malformed, or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API key lacks the required scope or allowlist gate","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Resource not found or not visible to this key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflict — idempotency key reuse or unique violation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error — check `request_id` and contact support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/catalogs":{"get":{"operationId":"listCatalogs","summary":"List catalogs visible to the sub-brand","description":"Returns catalogs the calling sub-brand can use:\n1. Catalogs owned exclusively by this sub-brand (`scope: \"sub_brand\"`).\n2. Brand-level catalogs the sub-brand has ACCEPTED (`scope: \"brand\"`).\n\nPending or rejected brand-level catalogs are filtered out — clients\nsee them as if they didn't exist.","tags":["Catalogs"],"security":[{"BearerApiKey":["catalogs:read"]}],"responses":{"200":{"description":"OK","headers":{"x-request-id":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Echo of the per-request id (`req_<32 hex>`)."},"x-ratelimit-limit":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Per-minute request cap for this key's tier."},"x-ratelimit-remaining":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Remaining requests in the current 60-second window."},"x-ratelimit-reset":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Unix timestamp (seconds) when the current window resets."}},"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Catalog"}},"page":{"$ref":"#/components/schemas/Page"}},"required":["data","page"],"additionalProperties":false}}}},"400":{"description":"Invalid request body or parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, malformed, or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API key lacks the required scope or allowlist gate","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Resource not found or not visible to this key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflict — idempotency key reuse or unique violation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error — check `request_id` and contact support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/catalogs/{id}":{"get":{"operationId":"getCatalog","summary":"Fetch a catalog by id","description":"Returns the catalog if it is visible to the calling sub-brand (own or accepted brand-level). 404 otherwise — pending/rejected brand-level catalogs return 404 to prevent enumeration.","tags":["Catalogs"],"security":[{"BearerApiKey":["catalogs:read"]}],"responses":{"200":{"description":"OK","headers":{"x-request-id":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Echo of the per-request id (`req_<32 hex>`)."},"x-ratelimit-limit":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Per-minute request cap for this key's tier."},"x-ratelimit-remaining":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Remaining requests in the current 60-second window."},"x-ratelimit-reset":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Unix timestamp (seconds) when the current window resets."}},"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Catalog"},"page":{"$ref":"#/components/schemas/Page"}},"required":["data"],"additionalProperties":false}}}},"400":{"description":"Invalid request body or parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, malformed, or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API key lacks the required scope or allowlist gate","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Resource not found or not visible to this key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflict — idempotency key reuse or unique violation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error — check `request_id` and contact support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/categories":{"get":{"operationId":"listCategories","summary":"List product categories","description":"Returns the global canonical category list. The default is `active`; pass `status=inactive` to inspect disabled categories.","tags":["Categories"],"security":[{"BearerApiKey":["categories:read"]}],"parameters":[{"in":"query","name":"status","schema":{"type":"string","enum":["active","inactive"]}}],"responses":{"200":{"description":"OK","headers":{"x-request-id":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Echo of the per-request id (`req_<32 hex>`)."},"x-ratelimit-limit":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Per-minute request cap for this key's tier."},"x-ratelimit-remaining":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Remaining requests in the current 60-second window."},"x-ratelimit-reset":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Unix timestamp (seconds) when the current window resets."}},"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Category"}},"page":{"$ref":"#/components/schemas/Page"}},"required":["data","page"],"additionalProperties":false}}}},"400":{"description":"Invalid request body or parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, malformed, or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API key lacks the required scope or allowlist gate","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Resource not found or not visible to this key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflict — idempotency key reuse or unique violation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error — check `request_id` and contact support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/brands/{brandId}/sub-brands/{subBrandId}":{"get":{"operationId":"getBrandSubBrand","summary":"Get brand and sub-brand metadata","description":"Returns the calling API key's sub-brand together with its parent brand. Both path ids must match the key scope.","tags":["Brands"],"security":[{"BearerApiKey":["brands:read"]}],"parameters":[],"responses":{"200":{"description":"OK","headers":{"x-request-id":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Echo of the per-request id (`req_<32 hex>`)."},"x-ratelimit-limit":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Per-minute request cap for this key's tier."},"x-ratelimit-remaining":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Remaining requests in the current 60-second window."},"x-ratelimit-reset":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Unix timestamp (seconds) when the current window resets."}},"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/BrandSubBrand"},"page":{"$ref":"#/components/schemas/Page"}},"required":["data"],"additionalProperties":false}}}},"400":{"description":"Invalid request body or parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, malformed, or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API key lacks the required scope or allowlist gate","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Resource not found or not visible to this key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflict — idempotency key reuse or unique violation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error — check `request_id` and contact support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/products":{"get":{"operationId":"searchProducts","summary":"Search products visible to the sub-brand","description":"Returns product families reachable via any catalog the sub-brand owns\nor has accepted. Product IDs are canonical UUIDv7 values from\n`product_families.id`; legacy product IDs are kept only in\nthe auxiliary resolution table.\n\nEach product includes `price_cents` / `currency` / `price_country`:\nthe final customer PVP resolved server-side from active listings in\nthe sub-brand's markets. The raw provider cost is never exposed.","tags":["Products"],"security":[{"BearerApiKey":["products:read"]}],"parameters":[{"in":"query","name":"q","schema":{"description":"Substring match across `name` and `slug` (case-insensitive).","type":"string","minLength":1,"maxLength":200},"description":"Substring match across `name` and `slug` (case-insensitive)."},{"in":"query","name":"category_id","schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},{"in":"query","name":"catalog_id","schema":{"description":"Restrict the search to a specific catalog. The catalog must already be visible to the calling sub-brand.","type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"description":"Restrict the search to a specific catalog. The catalog must already be visible to the calling sub-brand."},{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":100}},{"in":"query","name":"cursor","schema":{"type":"string"}}],"responses":{"200":{"description":"OK","headers":{"x-request-id":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Echo of the per-request id (`req_<32 hex>`)."},"x-ratelimit-limit":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Per-minute request cap for this key's tier."},"x-ratelimit-remaining":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Remaining requests in the current 60-second window."},"x-ratelimit-reset":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Unix timestamp (seconds) when the current window resets."}},"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Product"}},"page":{"$ref":"#/components/schemas/Page"}},"required":["data","page"],"additionalProperties":false}}}},"400":{"description":"Invalid request body or parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, malformed, or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API key lacks the required scope or allowlist gate","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Resource not found or not visible to this key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflict — idempotency key reuse or unique violation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error — check `request_id` and contact support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/products/{id}":{"get":{"operationId":"getProduct","summary":"Fetch a product family + its variants","description":"Returns the product family with its variants array. The path accepts\neither a legacy migrated product ID or the canonical UUIDv7.\n404 when the product is not visible to the calling sub-brand.\n\nThe top-level product fields include the final customer PVP\n(`price_cents`, `currency`, `price_country`) resolved through the\nprovider veil. Variant objects remain SKU/content-only in v1.","tags":["Products"],"security":[{"BearerApiKey":["products:read"]}],"responses":{"200":{"description":"OK","headers":{"x-request-id":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Echo of the per-request id (`req_<32 hex>`)."},"x-ratelimit-limit":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Per-minute request cap for this key's tier."},"x-ratelimit-remaining":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Remaining requests in the current 60-second window."},"x-ratelimit-reset":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Unix timestamp (seconds) when the current window resets."}},"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ProductDetail"},"page":{"$ref":"#/components/schemas/Page"}},"required":["data"],"additionalProperties":false}}}},"400":{"description":"Invalid request body or parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, malformed, or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API key lacks the required scope or allowlist gate","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Resource not found or not visible to this key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflict — idempotency key reuse or unique violation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error — check `request_id` and contact support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/orders":{"get":{"operationId":"listOrders","summary":"List orders","description":"Cursor-paginated list of orders for the calling sub-brand. Newest\nfirst. Filter by status, store id, or a creation-time window.\n\nBilling-layer fields (batch references, payment_reference, internal\nTPP sync state) are NOT exposed — those require a dedicated scope\nthat ships in a future release.","tags":["Orders"],"security":[{"BearerApiKey":["orders:read"]}],"parameters":[{"in":"query","name":"status","schema":{"type":"string","enum":["pending","processing","shipped","completed","failed","cancelled"]}},{"in":"query","name":"store_id","schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},{"in":"query","name":"customer_id","schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},{"in":"query","name":"q","schema":{"type":"string","minLength":1}},{"in":"query","name":"created_from","schema":{"description":"Inclusive lower bound on `created_at` (ISO-8601).","type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"description":"Inclusive lower bound on `created_at` (ISO-8601)."},{"in":"query","name":"created_to","schema":{"description":"Inclusive upper bound on `created_at` (ISO-8601).","type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"description":"Inclusive upper bound on `created_at` (ISO-8601)."},{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":100}},{"in":"query","name":"cursor","schema":{"type":"string"}}],"responses":{"200":{"description":"OK","headers":{"x-request-id":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Echo of the per-request id (`req_<32 hex>`)."},"x-ratelimit-limit":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Per-minute request cap for this key's tier."},"x-ratelimit-remaining":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Remaining requests in the current 60-second window."},"x-ratelimit-reset":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Unix timestamp (seconds) when the current window resets."}},"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Order"}},"page":{"$ref":"#/components/schemas/Page"}},"required":["data","page"],"additionalProperties":false}}}},"400":{"description":"Invalid request body or parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, malformed, or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API key lacks the required scope or allowlist gate","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Resource not found or not visible to this key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflict — idempotency key reuse or unique violation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error — check `request_id` and contact support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"reserveOrder","summary":"Reserve a new order","description":"Creates an order in the calling key's sub-brand from one or more\ncatalog listings. Each line item must reference an existing\n`provider_listing_markets` row available in the sub-brand's country\nAND an existing customer of the same sub-brand.\n\n**Idempotency-Key is required.** Replays with the same key + body\nreturn the cached response (the order WAS created by the first\nexecution). Replays with a different body return 409\n`idempotency_conflict`.\n\n**Pricing.** Each line's `unit_price_cents` is snapshotted from\n`provider_listing_markets.price` at creation. Margin precedence is\napplied internally: product override → brand default → provider\ndefault. The brand-discount-pct is also snapshotted so future\nconfig changes don't move historical totals.\n\n**Billing.**\n- `prepaid_balance` brands: the saldo is reserved atomically. If\n  insufficient and `allow_negative_balance=false`, the order is\n  created with `billing_status='blocked_balance'` — fulfilment\n  cannot proceed until the brand tops up. Watch the field on the\n  response.\n- `monthly_billing` brands: the order is attached to the current\n  open billing batch (created lazily for the period).\n- `pay_per_order` brands: the order is born with\n  `payment_status='pending_payment'` — fulfilment is gated until\n  the admin marks it paid in the backoffice.\n\n**Country gate.** All listings MUST match the sub-brand's country.\nA mixed-country payload returns 403 `forbidden`.\n\nPhase 3b ships catalog items only. Manual items (price negotiated\nclient-side, no `provider_listing_markets` row) land in a future\nrelease.\n\n**Transient-failure fallback.** A validated request that encounters\nan explicitly retryable database/infrastructure failure is durably\naccepted with HTTP 202. The response carries a\n`reservation_request_id`; poll\n`GET /api/v1/order-reservations/{id}` for completion. Validation,\nauthorization, catalog, stock, billing, and all unknown/non-retryable\nerrors remain ordinary 4xx/5xx errors and are never queued.","tags":["Orders"],"security":[{"BearerApiKey":["orders:create"]}],"parameters":[{"in":"header","name":"Idempotency-Key","schema":{"type":"string","minLength":1,"maxLength":64,"description":"Opaque ASCII string (max 64 chars) used to deduplicate replays for 24h.","example":"01HXYZABC-create-customer"},"required":true,"description":"Opaque ASCII string (max 64 chars) used to deduplicate replays for 24h."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"items":{"minItems":1,"maxItems":50,"type":"array","items":{"type":"object","properties":{"listing_market_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"UUID of the `provider_listing_markets` row to purchase. Get one from `GET /products/{id}` (variant + market resolution lands in v1.x) or from your catalog tooling."},"quantity":{"type":"integer","minimum":1,"maximum":1000,"description":"Units to purchase. Brand-level cart limit caps the total across items."},"customer_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"Customer who receives this line. MUST belong to the calling sub-brand. The customer's address snapshot fills the line's `shipping_address` and `recipient_*` fields."}},"required":["listing_market_id","quantity","customer_id"]},"description":"Line items. At least one; brand-level cart limit applies."},"store_id":{"description":"Optional store the order is attributed to. Analytics-only — orders are scoped to the sub-brand regardless of this value.","type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"currency":{"description":"ISO-4217 currency override. Defaults to the sub-brand's configured default (typically EUR).","type":"string","minLength":3,"maxLength":3}},"required":["items"]}}}},"responses":{"201":{"description":"Created","headers":{"x-request-id":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Echo of the per-request id (`req_<32 hex>`)."},"x-ratelimit-limit":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Per-minute request cap for this key's tier."},"x-ratelimit-remaining":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Remaining requests in the current 60-second window."},"x-ratelimit-reset":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Unix timestamp (seconds) when the current window resets."}},"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/OrderDetail"},"page":{"$ref":"#/components/schemas/Page"}},"required":["data"],"additionalProperties":false}}}},"202":{"description":"Accepted for retry after an explicitly recoverable infrastructure failure.","headers":{"x-request-id":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Echo of the per-request id (`req_<32 hex>`)."},"x-ratelimit-limit":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Per-minute request cap for this key's tier."},"x-ratelimit-remaining":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Remaining requests in the current 60-second window."},"x-ratelimit-reset":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Unix timestamp (seconds) when the current window resets."}},"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/OrderReservationRequest"},"page":{"$ref":"#/components/schemas/Page"}},"required":["data"],"additionalProperties":false}}}},"400":{"description":"Invalid request body or parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, malformed, or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API key lacks the required scope or allowlist gate","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Resource not found or not visible to this key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflict — idempotency key reuse or unique violation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error — check `request_id` and contact support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/orders/{id}":{"get":{"operationId":"getOrder","summary":"Fetch an order + its line items","description":"Returns the order header with its line items. Shipping details are also available via `GET /api/v1/orders/{id}/shipping` if you only need addresses + carrier info.","tags":["Orders"],"security":[{"BearerApiKey":["orders:read"]}],"responses":{"200":{"description":"OK","headers":{"x-request-id":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Echo of the per-request id (`req_<32 hex>`)."},"x-ratelimit-limit":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Per-minute request cap for this key's tier."},"x-ratelimit-remaining":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Remaining requests in the current 60-second window."},"x-ratelimit-reset":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Unix timestamp (seconds) when the current window resets."}},"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/OrderDetail"},"page":{"$ref":"#/components/schemas/Page"}},"required":["data"],"additionalProperties":false}}}},"400":{"description":"Invalid request body or parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, malformed, or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API key lacks the required scope or allowlist gate","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Resource not found or not visible to this key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflict — idempotency key reuse or unique violation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error — check `request_id` and contact support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"operationId":"cancelOrder","summary":"Cancel an order","description":"Transitions the order to `cancelled`. Only valid while the\norder is in `pending` or `processing` — later states return 409\n`conflict` with the current status in the error extras.\n\nPrepaid orders with an active balance reservation return 409 —\nrelease the reservation from the backoffice first. A future\nrelease will wire the inline release once the billing helpers\nexpose a non-session entrypoint.","tags":["Orders"],"security":[{"BearerApiKey":["orders:delete"]}],"responses":{"200":{"description":"OK","headers":{"x-request-id":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Echo of the per-request id (`req_<32 hex>`)."},"x-ratelimit-limit":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Per-minute request cap for this key's tier."},"x-ratelimit-remaining":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Remaining requests in the current 60-second window."},"x-ratelimit-reset":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Unix timestamp (seconds) when the current window resets."}},"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Order"},"page":{"$ref":"#/components/schemas/Page"}},"required":["data"],"additionalProperties":false}}}},"400":{"description":"Invalid request body or parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, malformed, or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API key lacks the required scope or allowlist gate","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Resource not found or not visible to this key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflict — idempotency key reuse or unique violation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error — check `request_id` and contact support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/orders/{id}/shipping":{"get":{"operationId":"getOrderShipping","summary":"Fetch per-item shipping details for an order","description":"Returns one entry per line item with the recipient + carrier\nfields, without the financial / billing fields. Convenient for\nlabel-printing flows and courier polling.","tags":["Orders"],"security":[{"BearerApiKey":["orders:read"]}],"responses":{"200":{"description":"OK","headers":{"x-request-id":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Echo of the per-request id (`req_<32 hex>`)."},"x-ratelimit-limit":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Per-minute request cap for this key's tier."},"x-ratelimit-remaining":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Remaining requests in the current 60-second window."},"x-ratelimit-reset":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Unix timestamp (seconds) when the current window resets."}},"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/OrderItemShipping"}},"page":{"$ref":"#/components/schemas/Page"}},"required":["data"],"additionalProperties":false}}}},"400":{"description":"Invalid request body or parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, malformed, or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API key lacks the required scope or allowlist gate","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Resource not found or not visible to this key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflict — idempotency key reuse or unique violation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error — check `request_id` and contact support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"operationId":"updateOrderShipping","summary":"Add or update an order's shipping address(es)","description":"Set the delivery address for an order. Two modes:\n\n**Grouped** — send `shipping_address` (+ optional recipient\nfields) at the top level and the SAME address is written to\nevery line item. The order's `shipping_mode` becomes `grouped`.\n\n```json\n{ \"shipping_address\": \"Calle Mayor 1\\n28013 Madrid\\nES\",\n  \"recipient_name\": \"Ana Pérez\" }\n```\n\n**Split** — send an `items` array, one entry per line item\n(addressed independently by `item_id`). The order's\n`shipping_mode` becomes `split`. Every `item_id` must belong\nto the order.\n\n```json\n{ \"items\": [\n  { \"item_id\": \"01H…\", \"shipping_address\": \"…\", \"recipient_name\": \"Ana\" },\n  { \"item_id\": \"01H…\", \"shipping_address\": \"…\", \"recipient_name\": \"Luis\" }\n] }\n```\n\nOnly recipient + address fields are touched — carrier and\ntracking stay fulfilment-side. Rejected with 409 once the\norder is `completed` or `cancelled`. Returns the full per-item\nshipping projection after the update.","tags":["Orders"],"security":[{"BearerApiKey":["orders:update"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"items":{"minItems":1,"maxItems":50,"type":"array","items":{"type":"object","properties":{"shipping_address":{"type":"string","minLength":1,"maxLength":2000,"description":"Free-form recipient address block (multiline allowed)."},"recipient_name":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}]},"recipient_email":{"anyOf":[{"type":"string","maxLength":255,"format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},{"type":"null"}]},"recipient_phone":{"anyOf":[{"type":"string","maxLength":40},{"type":"null"}]},"item_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"Line item this address applies to (must belong to the order)."}},"required":["shipping_address","item_id"]},"description":"One entry per line item — addresses sent independently."}},"required":["items"],"description":"Split — a distinct address per line item."},{"type":"object","properties":{"shipping_address":{"type":"string","minLength":1,"maxLength":2000,"description":"Free-form recipient address block (multiline allowed)."},"recipient_name":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}]},"recipient_email":{"anyOf":[{"type":"string","maxLength":255,"format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},{"type":"null"}]},"recipient_phone":{"anyOf":[{"type":"string","maxLength":40},{"type":"null"}]}},"required":["shipping_address"],"description":"Grouped — one address applied to every line item of the order."}]}}}},"responses":{"200":{"description":"OK","headers":{"x-request-id":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Echo of the per-request id (`req_<32 hex>`)."},"x-ratelimit-limit":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Per-minute request cap for this key's tier."},"x-ratelimit-remaining":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Remaining requests in the current 60-second window."},"x-ratelimit-reset":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Unix timestamp (seconds) when the current window resets."}},"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/OrderItemShipping"}},"page":{"$ref":"#/components/schemas/Page"}},"required":["data"],"additionalProperties":false}}}},"400":{"description":"Invalid request body or parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, malformed, or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API key lacks the required scope or allowlist gate","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Resource not found or not visible to this key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflict — idempotency key reuse or unique violation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error — check `request_id` and contact support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/order-reservations/{id}":{"get":{"operationId":"getOrderReservationRequest","summary":"Get an asynchronous order reservation","description":"Returns the durable status of a reservation accepted with HTTP 202\nby `POST /api/v1/orders`. The id is scoped to the calling key's\nsub-brand. When status becomes `completed`, use `order_id` with\n`GET /api/v1/orders/{id}`. `failed` is terminal and includes a\nsafe failure code/message.","tags":["Orders"],"security":[{"BearerApiKey":["orders:read"]}],"responses":{"200":{"description":"OK","headers":{"x-request-id":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Echo of the per-request id (`req_<32 hex>`)."},"x-ratelimit-limit":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Per-minute request cap for this key's tier."},"x-ratelimit-remaining":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Remaining requests in the current 60-second window."},"x-ratelimit-reset":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Unix timestamp (seconds) when the current window resets."}},"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/OrderReservationRequest"},"page":{"$ref":"#/components/schemas/Page"}},"required":["data"],"additionalProperties":false}}}},"400":{"description":"Invalid request body or parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, malformed, or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API key lacks the required scope or allowlist gate","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Resource not found or not visible to this key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflict — idempotency key reuse or unique violation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error — check `request_id` and contact support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/store-sessions":{"post":{"operationId":"createStoreSession","summary":"Create a storefront hand-off (SSO)","description":"Mints a short-lived, signed hand-off URL that logs the given CRM\nuser into the storefront. Resolve order: `external_id` then\n`email`, scoped to the calling key's sub-brand. Unknown users are\ncreated just-in-time.\n\nReturns 404 when `store_id` does not belong to the sub-brand.","tags":["Store sessions"],"security":[{"BearerApiKey":["store_sessions:create"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"store_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"Storefront to hand the user off to. Must belong to the calling key's sub-brand."},"external_id":{"type":"string","minLength":1,"maxLength":120,"description":"The CRM's identifier for the user. Resolved against `customers.external_id` within the sub-brand; created just-in-time if unknown."},"first_name":{"description":"Used only when JIT-creating the customer.","anyOf":[{"type":"string","maxLength":120},{"type":"null"}]},"last_name":{"description":"Used only when JIT-creating the customer.","anyOf":[{"type":"string","maxLength":120},{"type":"null"}]},"email":{"description":"Fallback lookup key + used when JIT-creating.","anyOf":[{"type":"string","maxLength":255,"format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},{"type":"null"}]},"ttl_seconds":{"description":"Hand-off URL lifetime. Clamped to [60, 900] seconds; default 300.","type":"integer","minimum":60,"maximum":900}},"required":["store_id","external_id"]}}}},"responses":{"201":{"description":"Created","headers":{"x-request-id":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Echo of the per-request id (`req_<32 hex>`)."},"x-ratelimit-limit":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Per-minute request cap for this key's tier."},"x-ratelimit-remaining":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Remaining requests in the current 60-second window."},"x-ratelimit-reset":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Unix timestamp (seconds) when the current window resets."}},"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/StoreSession"},"page":{"$ref":"#/components/schemas/Page"}},"required":["data"],"additionalProperties":false}}}},"400":{"description":"Invalid request body or parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, malformed, or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API key lacks the required scope or allowlist gate","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Resource not found or not visible to this key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflict — idempotency key reuse or unique violation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error — check `request_id` and contact support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/engine/events":{"post":{"operationId":"ingestEngineEvent","summary":"Deliver an event to the Engine","description":"Executes the event definition identified by `event_code` for the\nplayer referenced in `data` (resolved through the definition's\nconfigured player-id field, default `externalUserId`, matched\nagainst the customer's `external_id` within your sub-brand).\n\nA delivery that passes pre-flight always returns **200 with an\n`EngineEventResult`** — check `status`, don't assume `completed`:\n- `completed` — the points were applied to the wallet.\n- `skipped` — business rules declined: `outside_active_window`,\n  or a per-player velocity cap (`max_daily_reached` = daily limit,\n  `cooldown_active` = minimum interval between grants).\n- `failed` — the execution was recorded but did not apply:\n  `player_not_found`, `player_id_missing`, `insufficient_balance`,\n  or `internal_error` (transient).\n\n**Idempotency-Key header is REQUIRED** (this endpoint moves money).\nThe same value is also the BUSINESS idempotency key, so a\nredelivery through the webhook channel with the same id can never\ndouble-credit. A retry with the SAME key is normally answered by\nthe HTTP idempotency layer: the original response body verbatim\n(so `replayed` reads `false`) plus the `idempotent-replay: true`\nresponse header — key off the header. `replayed: true` in the body\nmarks a BUSINESS-layer replay (e.g. the same id already arrived\nvia webhook). Same key + a DIFFERENT body → 409\n`idempotency_conflict`.\n\nRetry semantics: a `failed` with `error_code: internal_error`\nreturns **500** and IS retryable with the same key (the pipeline\nreclaims the transient row); business `failed`/`skipped` are\nterminal and replay their frozen verdict.\n\nSending `points_override` requires the key to hold the\n`engine:overrides` scope AND the definition to be `external_bounded`\n(the value is clamped to the definition's min/max); otherwise the\ndelivery is rejected 400 `override_not_allowed`. A key with only\n`engine:events` keeps working for default-points and internal-only\nevents.\n\nPre-flight rejections (error ENVELOPE, no execution row):\n- 400 `override_not_allowed` — `points_override` sent without the\n  `engine:overrides` scope or to a definition that forbids it;\n  `invalid_points` (out of the definition's bounds); `payload_too_large`.\n- 403 — the Engine is not enabled for this brand.\n- 404 — unknown `event_code`.\n- 409 — the definition is not active.\n\nEvery delivery — completed, skipped, failed, replay, rejection —\nlands in the immutable Engine audit trail.","tags":["Engine"],"security":[{"BearerApiKey":["engine:events"]}],"parameters":[{"in":"header","name":"Idempotency-Key","schema":{"type":"string","minLength":1,"maxLength":64,"description":"Opaque ASCII string (max 64 chars) used to deduplicate replays for 24h.","example":"01HXYZABC-create-customer"},"required":true,"description":"Opaque ASCII string (max 64 chars) used to deduplicate replays for 24h."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"event_code":{"type":"string","pattern":"^[a-z0-9][a-z0-9_-]{0,79}$","description":"Code of the event definition to execute (unique per sub-brand)."},"data":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{},"description":"The event payload, stored verbatim on the execution and audit records. The player is resolved from the definition's configured field (default `externalUserId`)."},"points_override":{"description":"Optional per-event points value (signed non-zero integer, |points| <= 1,000,000). Only honoured when the definition sets `allow_override`; sending it to a definition that forbids it is rejected 400 `override_not_allowed` (before any execution row is created).","anyOf":[{"type":"number"},{"type":"null"}]},"correlation_id":{"description":"Optional caller tracing id (echoed back). Generated when absent.","anyOf":[{"type":"string","maxLength":64},{"type":"null"}]}},"required":["event_code","data"]}}}},"responses":{"200":{"description":"OK","headers":{"x-request-id":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Echo of the per-request id (`req_<32 hex>`)."},"x-ratelimit-limit":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Per-minute request cap for this key's tier."},"x-ratelimit-remaining":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Remaining requests in the current 60-second window."},"x-ratelimit-reset":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Unix timestamp (seconds) when the current window resets."}},"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/EngineEventResult"},"page":{"$ref":"#/components/schemas/Page"}},"required":["data"],"additionalProperties":false}}}},"400":{"description":"Invalid request body or parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, malformed, or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API key lacks the required scope or allowlist gate","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Resource not found or not visible to this key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflict — idempotency key reuse or unique violation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error — check `request_id` and contact support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/engine/webhooks/{code}":{"post":{"operationId":"ingestEngineWebhook","summary":"Deliver an event to the Engine via webhook","description":"Webhook-style ingestion: POST the provider payload verbatim; the\nevent definition code travels in the path. Runs the exact same\npipeline as `POST /engine/events` (same ledger, same audit trail,\nsame velocity caps).\n\nRedeliveries dedupe on the payload field configured on the\ndefinition (`idempotencyKeyField`; default `event_id`, then `id`)\n— a stable provider id is strongly recommended. Ids longer than\n64 chars are hashed deterministically, so dedup always holds.\n\n**FAIL-CLOSED anti-replay:** a delivery with NO usable id (no\nmatching payload field AND no `Idempotency-Key` header) is\n**rejected 400 `missing_idempotency_key`** rather than executed —\nwithout a key an attacker could replay the same body to farm\npoints. Always send a stable event id.\n\nResponds 200 with an `EngineEventResult` for every delivery that\npasses pre-flight — check `status` (`completed` / `skipped` for a\nrule or velocity cap / `failed`), don't assume `completed`.\n`replayed: true` marks a redelivery answered with the FROZEN\nfirst result. The same pre-flight rejections as `/engine/events`\napply (400 / 403 / 404 / 409; 500 for a transient `internal_error`\n— retry with the same id).","tags":["Engine"],"security":[{"BearerApiKey":["engine:events"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{},"description":"The provider's webhook payload, stored verbatim as the event payload. Redeliveries dedupe on the definition's configured `idempotencyKeyField` (default `event_id`, then `id`); over-long ids are hashed. A wallet-moving delivery that carries NO usable id and no `Idempotency-Key` header is rejected 400 `missing_idempotency_key` (fail-closed anti-replay) — always include a stable event id."}}}},"responses":{"200":{"description":"OK","headers":{"x-request-id":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Echo of the per-request id (`req_<32 hex>`)."},"x-ratelimit-limit":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Per-minute request cap for this key's tier."},"x-ratelimit-remaining":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Remaining requests in the current 60-second window."},"x-ratelimit-reset":{"schema":{"def":{"type":"string"},"type":"string","format":null,"minLength":null,"maxLength":null},"description":"Unix timestamp (seconds) when the current window resets."}},"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/EngineEventResult"},"page":{"$ref":"#/components/schemas/Page"}},"required":["data"],"additionalProperties":false}}}},"400":{"description":"Invalid request body or parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, malformed, or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API key lacks the required scope or allowlist gate","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Resource not found or not visible to this key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflict — idempotency key reuse or unique violation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error — check `request_id` and contact support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"components":{"schemas":{"Page":{"type":"object","properties":{"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Opaque cursor for the next page, or `null` when on the last page."},"has_more":{"type":"boolean","description":"Whether there is at least one more page."},"total":{"description":"Total row count — only present on endpoints that opt into the extra query.","type":"integer","minimum":0,"maximum":9007199254740991}},"required":["next_cursor","has_more"],"additionalProperties":false,"description":"Pagination envelope. `next_cursor` is `null` on the last page; clients can use it as the sentinel."},"Error":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["unauthenticated","wrong_environment","forbidden","not_found","invalid_request","idempotency_conflict","conflict","rate_limited","internal_error"],"description":"Canonical error code — part of the public contract."},"message":{"type":"string","description":"Human-readable description of the error."},"field_errors":{"description":"Per-field validation messages keyed by dotted path. Present on `invalid_request` responses.","type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"array","items":{"type":"string"}}},"request_id":{"type":"string","description":"Per-request id (also echoed in the `X-Request-Id` response header)."}},"required":["code","message","request_id"],"additionalProperties":false}},"required":["error"],"additionalProperties":false,"description":"Returned for any non-2xx response."},"Customer":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"UUIDv7 — stable, time-sortable identifier."},"external_id":{"description":"Caller-supplied identifier (typically the casino platform's user id). Unique per sub-brand.","anyOf":[{"type":"string"},{"type":"null"}]},"segment_id":{"description":"Free-text segmentation tag (max 36 chars). Set by the brand operator for analytics.","anyOf":[{"type":"string"},{"type":"null"}]},"first_name":{"type":"string","description":"Given name. Required, max 120 chars."},"last_name":{"type":"string","description":"Family name. Required, max 120 chars."},"nickname":{"description":"Public alias / handle on the casino platform. Not unique.","anyOf":[{"type":"string"},{"type":"null"}]},"email":{"description":"Customer email. Optional and NOT unique within a sub-brand (use `external_id` as the stable handle).","anyOf":[{"type":"string","format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},{"type":"null"}]},"phone":{"description":"Free-form phone string. Optional, max 40 chars.","anyOf":[{"type":"string"},{"type":"null"}]},"address_line":{"description":"Street + number. Most granular component of the hierarchical address.","anyOf":[{"type":"string"},{"type":"null"}]},"postal_code":{"description":"ZIP / CP. Optional, max 40 chars.","anyOf":[{"type":"string"},{"type":"null"}]},"zone":{"description":"Neighbourhood / district — the most granular subdivision.","anyOf":[{"type":"string"},{"type":"null"}]},"locality":{"description":"Town / village. Sits between zone and city.","anyOf":[{"type":"string"},{"type":"null"}]},"city":{"description":"Municipality or principal city.","anyOf":[{"type":"string"},{"type":"null"}]},"state":{"description":"State, province or first-level administrative area.","anyOf":[{"type":"string"},{"type":"null"}]},"country":{"description":"ISO 3166-1 alpha-2.","anyOf":[{"type":"string","minLength":2,"maxLength":2},{"type":"null"}]},"preferred_lang":{"description":"Preferred transactional-email language.","anyOf":[{"type":"string","enum":["en","es","pt","it","fr"]},{"type":"null"}]},"status":{"type":"string","enum":["active","watch","restricted","banned"],"description":"Moderation lifecycle. `active` is the unrestricted default; `watch` is a soft flag; `restricted` blocks some mutations; `banned` blocks all."},"status_note":{"description":"Free-text rationale attached to the current moderation status. Visible to operators in the backoffice — surface it to your support team but NOT directly to end consumers.","anyOf":[{"type":"string"},{"type":"null"}]},"status_changed_at":{"description":"ISO-8601 UTC timestamp of the last status change. Null when the customer has been in their initial status since creation.","anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},{"type":"null"}]},"platform_registered_at":{"description":"When the end-consumer registered on the casino platform — NOT when the TPP row was created. Null when the CRM doesn't supply it.","anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},{"type":"null"}]},"created_at":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$","description":"ISO-8601 UTC timestamp when the customer row was created in TPP. NOT the casino-platform registration date (see `platform_registered_at`)."},"updated_at":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$","description":"ISO-8601 UTC timestamp of the most recent profile / status change."}},"required":["id","first_name","last_name","status","created_at","updated_at"],"additionalProperties":false,"description":"An end-consumer of a sub-brand's storefront. The same person can exist as separate Customer rows under different sub-brands (they're independent tenants).","example":{"id":"01933b1c-1f00-7000-8000-000000000001","external_id":"casino-user-42","segment_id":"vip","first_name":"Ana","last_name":"Pérez","nickname":"anita_p","email":"ana.perez@example.com","phone":"+34600111222","address_line":"Calle Mayor 12, 3º A","postal_code":"28013","zone":"Centro","locality":"Madrid","city":"Madrid","state":"Madrid","country":"ES","preferred_lang":"es","status":"active","status_note":null,"status_changed_at":null,"platform_registered_at":"2026-01-15T10:00:00.000Z","created_at":"2026-05-01T09:30:00.000Z","updated_at":"2026-05-20T11:45:00.000Z"}},"CoinWallet":{"type":"object","properties":{"customer_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"balance":{"type":"integer","minimum":0,"maximum":9007199254740991,"description":"Current balance. `0` is returned when the wallet row has never been touched (no movements ever)."},"recent_movements":{"maxItems":10,"type":"array","items":{"$ref":"#/components/schemas/CoinMovement"},"description":"Up to the 10 most recent movements, newest first."}},"required":["customer_id","balance","recent_movements"],"additionalProperties":false,"example":{"customer_id":"01933b1c-1f00-7000-8000-000000000001","balance":500,"recent_movements":[{"id":"01933b1c-2300-7000-8000-000000000090","delta":500,"balance_after":500,"reason":"Signup bonus","created_at":"2026-05-27T18:00:00.000Z"}]}},"CoinMovement":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"Movement row id (PK of `customer_coin_movements`)."},"delta":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Signed adjustment that was applied. Positive = credit, negative = debit."},"balance_after":{"type":"integer","minimum":0,"maximum":9007199254740991,"description":"Snapshot of the balance immediately after this movement landed."},"reason":{"anyOf":[{"type":"string"},{"type":"null"}]},"created_at":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"}},"required":["id","delta","balance_after","created_at"],"additionalProperties":false,"example":{"id":"01933b1c-2300-7000-8000-000000000090","delta":500,"balance_after":500,"reason":"Signup bonus","created_at":"2026-05-27T18:00:00.000Z"}},"CoinAdjustment":{"type":"object","properties":{"customer_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"Customer whose wallet was adjusted."},"movement_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"Unique id of the movement row created by this adjustment. Useful for cross-referencing in audit / support flows."},"delta":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Signed adjustment that was applied. Positive = credit (`+`), negative = debit (`-`). Always non-zero."},"balance_before":{"type":"integer","minimum":0,"maximum":9007199254740991,"description":"Wallet balance BEFORE the adjustment landed. `0` for the very first adjustment on a new customer."},"balance_after":{"type":"integer","minimum":0,"maximum":9007199254740991,"description":"Wallet balance AFTER the adjustment landed. This is the value cached on `customer_coin_balances.balance` for subsequent `GET` reads."},"reason":{"description":"Free-text reason (max 500 chars) supplied by the caller. Surfaced verbatim in the backoffice movements table.","anyOf":[{"type":"string"},{"type":"null"}]},"created_at":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$","description":"ISO-8601 UTC timestamp when the movement was persisted."}},"required":["customer_id","movement_id","delta","balance_before","balance_after","created_at"],"additionalProperties":false,"description":"Result of a single coin adjustment. Replays via Idempotency-Key return THIS exact response (the cached balance from the original execution), NOT a fresh read.","example":{"customer_id":"01933b1c-1f00-7000-8000-000000000001","movement_id":"01933b1c-2300-7000-8000-000000000090","delta":500,"balance_before":0,"balance_after":500,"reason":"Signup bonus","created_at":"2026-05-27T18:00:00.000Z"}},"BulkCoinsResult":{"type":"object","properties":{"dry_run":{"type":"boolean","description":"True when this was a preview — no balances were changed."},"matched_count":{"type":"integer","minimum":0,"maximum":9007199254740991,"description":"Customers matched by the filter, scoped to the sub-brand."},"affected_count":{"description":"Customers actually credited/debited. Null/absent on a dry run.","anyOf":[{"type":"integer","minimum":0,"maximum":9007199254740991},{"type":"null"}]},"skipped":{"type":"array","items":{"type":"object","properties":{"customer_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"reason":{"type":"string","description":"Why this customer was skipped, e.g. `insufficient_balance`."},"balance_before":{"type":"integer","minimum":0,"maximum":9007199254740991}},"required":["customer_id","reason","balance_before"],"additionalProperties":false},"description":"Customers skipped without applying the delta (e.g. a debit that would go negative). Empty for credits."},"delta":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Signed adjustment applied to each matched customer."},"reason":{"anyOf":[{"type":"string"},{"type":"null"}]},"sample":{"description":"On a dry run, up to 10 matched customers as a preview.","anyOf":[{"maxItems":10,"type":"array","items":{"type":"object","properties":{"customer_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"email":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["customer_id"],"additionalProperties":false}},{"type":"null"}]}},"required":["dry_run","matched_count","skipped","delta"],"additionalProperties":false,"description":"Outcome of a bulk coin adjustment over a customer filter. Idempotent: a replay with the same Idempotency-Key returns this exact response.","example":{"dry_run":false,"matched_count":214,"affected_count":214,"skipped":[],"delta":1000,"reason":"Loyalty campaign — registered before June 2026"}},"Store":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"Unique store identifier (UUIDv7, time-sortable)."},"name":{"type":"string","description":"Display name shown in the backoffice and as the storefront's header."},"slug":{"type":"string","description":"URL slug — **globally unique** across all stores (not just within this sub-brand). The public storefront lives at `/s/<slug>`."},"status":{"type":"string","enum":["draft","active","inactive"],"description":"Lifecycle. Only `active` stores are visible to public storefront visitors."},"default_lang":{"type":"string","minLength":2,"maxLength":2,"description":"ISO 639-1 default language for the storefront."},"payment_mode":{"type":"string","enum":["card","points","order_only","off_price"],"description":"How customers pay through this store. `card` = checkout via card, `points` = redeem points only, `order_only` = manual / off-platform fulfilment, `off_price` = simplified storefront with hidden prices and no payment step (behaves like `order_only` for fulfilment)."},"points_rate":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Points awarded per currency unit spent. Storefront uses this to render the points label."},"integration_type":{"type":"string","enum":["marketplace","api","captainup","smartico"],"description":"Which backend powers the store. `marketplace` is the built-in TPP storefront."},"allow_testing_providers":{"type":"boolean","description":"When true, this store ALSO surfaces products from providers in `testing` lifecycle (not just `live`). Useful for QA sub-brands; defaults to `false` in production stores."},"created_at":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$","description":"ISO-8601 UTC timestamp when the store was created."},"updated_at":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$","description":"ISO-8601 UTC timestamp of the most recent change."}},"required":["id","name","slug","status","default_lang","payment_mode","points_rate","integration_type","allow_testing_providers","created_at","updated_at"],"additionalProperties":false,"description":"A storefront under a sub-brand. Stores are not creatable via API in v1 (no write scope yet).","example":{"id":"01933b1c-1f00-7000-8000-000000000010","name":"Codere Premium ES","slug":"codere-premium-es","status":"active","default_lang":"es","payment_mode":"card","points_rate":10,"integration_type":"marketplace","allow_testing_providers":false,"created_at":"2026-04-01T10:00:00.000Z","updated_at":"2026-05-15T11:30:00.000Z"}},"Catalog":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"Unique catalog identifier (UUIDv7, time-sortable)."},"name":{"type":"string","description":"Operator-set display name."},"description":{"description":"Free-text description. Surfaced in the backoffice catalog picker; may be null for legacy rows.","anyOf":[{"type":"string"},{"type":"null"}]},"scope":{"type":"string","enum":["brand","sub_brand"],"description":"`brand` = catalog owned by the brand and accepted by this sub-brand (visible because the acceptance ledger is in `accepted` state). `sub_brand` = catalog owned exclusively by this sub-brand (no acceptance flow needed)."},"brand_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"Brand that owns the catalog. For sub-brand-scoped catalogs this is the parent brand of the calling sub-brand."},"created_at":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$","description":"ISO-8601 UTC timestamp when the catalog was created."}},"required":["id","name","scope","brand_id","created_at"],"additionalProperties":false,"example":{"id":"01933b1c-1f00-7000-8000-000000000020","name":"Premium Q2 2026","description":"VIP rewards curated quarterly","scope":"sub_brand","brand_id":"01933b1c-1f00-7000-8000-000000000900","created_at":"2026-04-01T10:00:00.000Z"}},"Category":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"Canonical category UUIDv7."},"name":{"type":"string","description":"Canonical category display name."},"slug":{"type":"string","description":"Stable URL-safe category slug."},"icon":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Optional Lucide icon name configured by an operator."},"status":{"type":"string","enum":["active","inactive"],"description":"Whether products in this category may be surfaced."}},"required":["id","name","slug","icon","status"],"additionalProperties":false,"example":{"id":"01933b1c-1f00-7000-8000-000000000030","name":"Technology & Electronics","slug":"technology-electronics","icon":"laptop","status":"active"}},"BrandSubBrand":{"type":"object","properties":{"brand_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"brand_name":{"type":"string"},"sub_brand_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"sub_brand_name":{"type":"string"},"slug":{"type":"string"},"country":{"type":"string","minLength":2,"maxLength":2},"logo":{"anyOf":[{"type":"string"},{"type":"null"}]},"default_currency":{"type":"string","minLength":3,"maxLength":3},"created_at":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"}},"required":["brand_id","brand_name","sub_brand_id","sub_brand_name","slug","country","default_currency","created_at"],"additionalProperties":false},"Product":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"Unique product family identifier (UUIDv7, time-sortable)."},"name":{"type":"string","description":"Display name for the product family."},"slug":{"type":"string","description":"URL-safe slug for the product family. Unique within the TPP catalog."},"category_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"Category the family belongs to. Use `/products?category_id=` to list other products in the same category."},"image":{"description":"URL of the raw product image (PNG/JPG). May be null when no image has been uploaded yet.","anyOf":[{"type":"string"},{"type":"null"}]},"image_clean":{"description":"URL of the background-removed PNG produced by the cron worker. Null until the cron processes the original. Prefer this over `image` for product galleries — it composites cleanly on any background.","anyOf":[{"type":"string"},{"type":"null"}]},"status":{"type":"string","enum":["draft","active","paused","archived"],"description":"Publication status. Only `active` products are sellable. `draft` = pending validation; `paused` = temporarily hidden; `archived` = discontinued."},"is_custom":{"type":"boolean","description":"`true` for brand-created products (not sourced from the TPP shared catalogue). Custom products are scoped to the owning brand or sub-brand."},"price_cents":{"description":"Final customer PVP in minor units, after margin/commission/rounding and provider veil. Public product endpoints filter out families without an active priced listing; null is reserved for defensive legacy rows.","anyOf":[{"type":"integer","minimum":0,"maximum":9007199254740991},{"type":"null"}]},"currency":{"description":"ISO-4217 currency code for `price_cents`. Null when `price_cents` is null.","anyOf":[{"type":"string","minLength":3,"maxLength":3},{"type":"null"}]},"price_country":{"description":"Lowercase ISO-3166 market country used to resolve `price_cents`. The sub-brand primary market is preferred; secondary markets are used only when no primary-market listing is available.","anyOf":[{"type":"string","minLength":2,"maxLength":2},{"type":"null"}]}},"required":["id","name","slug","category_id","status","is_custom"],"additionalProperties":false,"description":"A product family visible to the calling sub-brand via an accepted/owned catalog. Obsolete pre-v7 product UUIDs are resolved server-side but are not exposed as the canonical identifier.","example":{"id":"01933b1c-1f00-7000-8000-000000000030","name":"PlayStation 5","slug":"playstation-5","category_id":"01933b1c-1f00-7000-8000-000000000080","image":"/products/ps5.jpg","image_clean":"/products/ps5-clean.png","status":"active","is_custom":false,"price_cents":54999,"currency":"EUR","price_country":"es"}},"ProductDetail":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"Unique product family identifier (UUIDv7, time-sortable)."},"name":{"type":"string","description":"Display name for the product family."},"slug":{"type":"string","description":"URL-safe slug for the product family. Unique within the TPP catalog."},"category_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"Category the family belongs to. Use `/products?category_id=` to list other products in the same category."},"image":{"description":"URL of the raw product image (PNG/JPG). May be null when no image has been uploaded yet.","anyOf":[{"type":"string"},{"type":"null"}]},"image_clean":{"description":"URL of the background-removed PNG produced by the cron worker. Null until the cron processes the original. Prefer this over `image` for product galleries — it composites cleanly on any background.","anyOf":[{"type":"string"},{"type":"null"}]},"status":{"type":"string","enum":["draft","active","paused","archived"],"description":"Publication status. Only `active` products are sellable. `draft` = pending validation; `paused` = temporarily hidden; `archived` = discontinued."},"is_custom":{"type":"boolean","description":"`true` for brand-created products (not sourced from the TPP shared catalogue). Custom products are scoped to the owning brand or sub-brand."},"price_cents":{"description":"Final customer PVP in minor units, after margin/commission/rounding and provider veil. Public product endpoints filter out families without an active priced listing; null is reserved for defensive legacy rows.","anyOf":[{"type":"integer","minimum":0,"maximum":9007199254740991},{"type":"null"}]},"currency":{"description":"ISO-4217 currency code for `price_cents`. Null when `price_cents` is null.","anyOf":[{"type":"string","minLength":3,"maxLength":3},{"type":"null"}]},"price_country":{"description":"Lowercase ISO-3166 market country used to resolve `price_cents`. The sub-brand primary market is preferred; secondary markets are used only when no primary-market listing is available.","anyOf":[{"type":"string","minLength":2,"maxLength":2},{"type":"null"}]},"variants":{"type":"array","items":{"$ref":"#/components/schemas/ProductVariant"}}},"required":["id","name","slug","category_id","status","is_custom","variants"],"additionalProperties":false},"ProductVariant":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"Unique variant identifier (UUIDv7, time-sortable)."},"name":{"type":"string","description":"Variant display name (e.g. `PS5 Disc Edition`)."},"variant_label":{"type":"string","description":"Short marketing label shown in pickers (e.g. `Disc`, `Digital`, `256GB`)."},"sku":{"type":"string","description":"Stock-keeping unit. Set by the operator at variant creation; not guaranteed unique across families."},"image":{"description":"Variant-specific image, falls back to the family image when null.","anyOf":[{"type":"string"},{"type":"null"}]},"image_clean":{"description":"Background-removed variant image. Falls back to the family image when null.","anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","name","variant_label","sku"],"additionalProperties":false},"Order":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"Unique order identifier (UUIDv7, time-sortable)."},"store_id":{"description":"Store the order was attributed to. May be null for orders created outside any store (e.g. operator-initiated).","anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},{"type":"null"}]},"status":{"type":"string","enum":["pending","processing","shipped","completed","failed","cancelled"],"description":"Fulfilment lifecycle. `pending` (just created) → `processing` (operator picked up) → `shipped` (in transit) → `completed` (delivered + closed). Terminal failure states: `failed`, `cancelled`. Cancellation is only valid while `pending` or `processing`."},"total_amount_cents":{"type":"integer","minimum":0,"maximum":9007199254740991,"description":"Total cost to the brand, in minor units of `currency`. e.g. 1500 + currency 'EUR' = €15.00. Includes margins and discount snapshots from creation time — historical totals never move when config changes."},"currency":{"type":"string","minLength":3,"maxLength":3,"description":"ISO-4217 code snapshotted at order creation. The order's `total_amount_cents` is denominated in this currency."},"shipping_mode":{"type":"string","enum":["grouped","split"],"description":"`grouped` = single shipping address applies to the whole order; items can have quantity > 1. `split` = each unit ships independently (items with quantity > 1 are expanded into N rows of qty 1, each addressable separately)."},"payment_status":{"type":"string","enum":["not_required","pending_payment","paid","failed","refunded","cancelled"],"description":"Per-order payment state. `not_required` for prepaid_balance and monthly_billing models; `pending_payment` → `paid` (or `failed`/`refunded`) for pay_per_order brands. Fulfilment is gated until `not_required` or `paid`."},"billing_status":{"type":"string","enum":["unbilled","in_batch","invoiced","paid","not_applicable","blocked_balance"],"description":"Lifecycle from the billing layer's POV. Orthogonal to `status`. `blocked_balance` = prepaid order created but balance was insufficient — operator must top up before fulfilment can proceed. `not_applicable` = the order's billing model doesn't track this field."},"paid_at":{"description":"ISO-8601 UTC timestamp when the order was marked paid. Null until payment lands (pay-per-order) or when the billing model doesn't track payment per-order.","anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},{"type":"null"}]},"created_at":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$","description":"ISO-8601 UTC timestamp when the order was reserved."},"status_url":{"description":"Absolute, ready-to-use link to the customer-facing order-status page, already carrying the signed `status_token`. Send it to the shopper (email / CRM) — it opens WITHOUT a session and reveals only this order. Null when the order has no store, or when `STOREFRONT_BASE_URL` is not configured server-side (in which case build the URL yourself from `status_token`).","anyOf":[{"type":"string","format":"uri"},{"type":"null"}]},"status_token":{"description":"Signed, permanent (no expiry) per-order access token. Append it as `?t=<token>` to the storefront order-status path `/s/<store_slug>/order/<id>` to build a link that opens without a session. Prefer `status_url` when present. Null when the order has no store or no signing secret is configured.","anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","status","total_amount_cents","currency","shipping_mode","payment_status","billing_status","created_at"],"additionalProperties":false,"example":{"id":"01933b1c-2000-7000-8000-000000000050","store_id":"01933b1c-1f00-7000-8000-000000000010","status":"pending","total_amount_cents":14999,"currency":"EUR","shipping_mode":"grouped","payment_status":"not_required","billing_status":"in_batch","paid_at":null,"created_at":"2026-05-27T18:00:00.000Z","status_url":"https://store.thepowerplugin.com/s/acme/order/01933b1c-2000-7000-8000-000000000050?t=eyJ2IjoxL... .sig","status_token":"eyJ2IjoxLCJvcmRlcklkIjoi... .sig"}},"OrderDetail":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"Unique order identifier (UUIDv7, time-sortable)."},"store_id":{"description":"Store the order was attributed to. May be null for orders created outside any store (e.g. operator-initiated).","anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},{"type":"null"}]},"status":{"type":"string","enum":["pending","processing","shipped","completed","failed","cancelled"],"description":"Fulfilment lifecycle. `pending` (just created) → `processing` (operator picked up) → `shipped` (in transit) → `completed` (delivered + closed). Terminal failure states: `failed`, `cancelled`. Cancellation is only valid while `pending` or `processing`."},"total_amount_cents":{"type":"integer","minimum":0,"maximum":9007199254740991,"description":"Total cost to the brand, in minor units of `currency`. e.g. 1500 + currency 'EUR' = €15.00. Includes margins and discount snapshots from creation time — historical totals never move when config changes."},"currency":{"type":"string","minLength":3,"maxLength":3,"description":"ISO-4217 code snapshotted at order creation. The order's `total_amount_cents` is denominated in this currency."},"shipping_mode":{"type":"string","enum":["grouped","split"],"description":"`grouped` = single shipping address applies to the whole order; items can have quantity > 1. `split` = each unit ships independently (items with quantity > 1 are expanded into N rows of qty 1, each addressable separately)."},"payment_status":{"type":"string","enum":["not_required","pending_payment","paid","failed","refunded","cancelled"],"description":"Per-order payment state. `not_required` for prepaid_balance and monthly_billing models; `pending_payment` → `paid` (or `failed`/`refunded`) for pay_per_order brands. Fulfilment is gated until `not_required` or `paid`."},"billing_status":{"type":"string","enum":["unbilled","in_batch","invoiced","paid","not_applicable","blocked_balance"],"description":"Lifecycle from the billing layer's POV. Orthogonal to `status`. `blocked_balance` = prepaid order created but balance was insufficient — operator must top up before fulfilment can proceed. `not_applicable` = the order's billing model doesn't track this field."},"paid_at":{"description":"ISO-8601 UTC timestamp when the order was marked paid. Null until payment lands (pay-per-order) or when the billing model doesn't track payment per-order.","anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},{"type":"null"}]},"created_at":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$","description":"ISO-8601 UTC timestamp when the order was reserved."},"status_url":{"description":"Absolute, ready-to-use link to the customer-facing order-status page, already carrying the signed `status_token`. Send it to the shopper (email / CRM) — it opens WITHOUT a session and reveals only this order. Null when the order has no store, or when `STOREFRONT_BASE_URL` is not configured server-side (in which case build the URL yourself from `status_token`).","anyOf":[{"type":"string","format":"uri"},{"type":"null"}]},"status_token":{"description":"Signed, permanent (no expiry) per-order access token. Append it as `?t=<token>` to the storefront order-status path `/s/<store_slug>/order/<id>` to build a link that opens without a session. Prefer `status_url` when present. Null when the order has no store or no signing secret is configured.","anyOf":[{"type":"string"},{"type":"null"}]},"items":{"type":"array","items":{"$ref":"#/components/schemas/OrderItem"}}},"required":["id","status","total_amount_cents","currency","shipping_mode","payment_status","billing_status","created_at","items"],"additionalProperties":false},"OrderItem":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"Unique order-item identifier (UUIDv7)."},"public_id":{"type":"string","description":"Human-friendly id derived at creation (format `OI-<chars>`). Stable, used in invoices and customer-facing emails."},"product_name":{"type":"string","description":"Snapshot of the product name at order time. Subsequent renames to the underlying product family do NOT update this — historical integrity."},"sku":{"type":"string","description":"Snapshot of the variant SKU at order time. Same historical-integrity rule as `product_name`."},"image":{"description":"Snapshot of the image URL at order time. Use this for receipts/invoices so the displayed product matches what was ordered.","anyOf":[{"type":"string"},{"type":"null"}]},"quantity":{"type":"integer","minimum":1,"maximum":9007199254740991,"description":"Units in this line. For `shipping_mode='split'` orders, items are expanded so each row has `quantity: 1`."},"unit_price_cents":{"type":"integer","minimum":0,"maximum":9007199254740991,"description":"Per-unit SELL price (cost + margin) in minor units of the order's `currency`, snapshotted from the provider listing at order time and veiled — the raw provider cost is never exposed."},"source_price_cents":{"description":"Per-unit SELL price in minor units of `source_currency`, for lines sourced in a market whose currency differs from the order's `currency`. The native-currency counterpart of `unit_price_cents` (`unit_price_cents ≈ round(source_price_cents * exchange_rate)`), already veiled (cost + margin, never the raw provider cost). Null only for legacy lines created before multi-currency capture; same-currency lines carry it equal to `unit_price_cents`.","anyOf":[{"type":"integer","minimum":0,"maximum":9007199254740991},{"type":"null"}]},"source_currency":{"description":"ISO-4217 code `source_price_cents` is denominated in (the sourcing market's currency), snapshotted at order time. Equals the order's `currency` for same-currency lines; null only for legacy pre-capture lines.","anyOf":[{"type":"string","minLength":3,"maxLength":3},{"type":"null"}]},"exchange_rate":{"description":"Capture-time multiplier converting `source_price_cents` into the order's `currency` (`unit_price_cents ≈ round(source_price_cents * exchange_rate)`). Frozen at order time so a later rate move never rewrites history. `1` for same-currency lines; null only for legacy pre-capture lines.","anyOf":[{"type":"number","exclusiveMinimum":0},{"type":"null"}]},"status":{"type":"string","enum":["pending","processing","shipped","delivered"],"description":"Per-line fulfilment status. Note: this is the LINE's status, distinct from the ORDER's status."},"is_completed":{"type":"boolean","description":"Convenience flag — `true` once the line reaches `delivered`."}},"required":["id","public_id","product_name","sku","quantity","unit_price_cents","status","is_completed"],"additionalProperties":false,"example":{"id":"01933b1c-2100-7000-8000-000000000070","public_id":"OI-ABC123","product_name":"PlayStation 5 Disc Edition","sku":"PS5-DISC-EU","image":"/products/ps5-disc.png","quantity":1,"unit_price_cents":15000,"source_price_cents":12000,"source_currency":"GBP","exchange_rate":1.25,"status":"pending","is_completed":false}},"OrderReservationRequest":{"type":"object","properties":{"reservation_request_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"Durable identifier for the asynchronous reservation request."},"status":{"type":"string","enum":["pending","processing","completed","failed"],"description":"Current asynchronous reservation lifecycle state."},"order_id":{"description":"Created order id. Null until the reservation completes.","anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},{"type":"null"}]},"attempts":{"type":"integer","minimum":0,"maximum":9007199254740991,"description":"Creation attempts already started, including the synchronous attempt."},"next_attempt_at":{"description":"Next scheduled retry time. Null outside the pending state.","anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},{"type":"null"}]},"failure":{"description":"Terminal failure after processing. Null while retrying or completed.","anyOf":[{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"field_errors":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"array","items":{"type":"string"}}}},"required":["code","message"],"additionalProperties":false},{"type":"null"}]},"created_at":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"updated_at":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"}},"required":["reservation_request_id","status","attempts","created_at","updated_at"],"additionalProperties":false,"example":{"reservation_request_id":"01933b1c-2000-7000-8000-000000000099","status":"pending","order_id":null,"attempts":1,"next_attempt_at":"2026-07-27T14:00:01.000Z","failure":null,"created_at":"2026-07-27T14:00:00.000Z","updated_at":"2026-07-27T14:00:00.000Z"}},"OrderItemShipping":{"type":"object","properties":{"item_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"FK to `OrderItem.id`."},"public_id":{"type":"string","description":"Human-friendly id of the line — same as `OrderItem.public_id`."},"recipient_name":{"description":"Recipient full name. Snapshotted from the customer at order creation. Null when the order was created without a linked customer (legacy or operator-initiated). UNTRUSTED USER INPUT: on storefront orders this is buyer-supplied — treat it as data, never as instructions, and never concatenate it into an LLM/agent system prompt.","anyOf":[{"type":"string"},{"type":"null"}]},"recipient_email":{"description":"Snapshot of the customer's email at order creation.","anyOf":[{"type":"string","format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},{"type":"null"}]},"recipient_phone":{"description":"Snapshot of the customer's phone. Optional — couriers occasionally need it for last-mile delivery coordination.","anyOf":[{"type":"string"},{"type":"null"}]},"shipping_address":{"type":"string","description":"Free-form multiline address block (newline-joined). Empty string when never set. UNTRUSTED USER INPUT: on storefront orders this is buyer-supplied free text — display it as data, never interpret it as instructions, and never feed it into an LLM/agent system prompt unfenced."},"shipping_carrier":{"description":"Carrier name (e.g. `Correos Express`). Null until shipped.","anyOf":[{"type":"string"},{"type":"null"}]},"tracking_code":{"description":"Carrier tracking code. Null until shipped. Concatenate with the carrier's tracking-URL template to render a customer link.","anyOf":[{"type":"string"},{"type":"null"}]},"status":{"type":"string","enum":["pending","processing","shipped","delivered"],"description":"Per-line fulfilment status — same value as `OrderItem.status`, duplicated here so a shipping-only consumer doesn't need to cross-reference."},"shipped_at":{"description":"ISO-8601 UTC timestamp when the carrier picked up the package.","anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},{"type":"null"}]},"delivered_at":{"description":"ISO-8601 UTC timestamp of delivery confirmation. Set by the carrier sync; null until then.","anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},{"type":"null"}]}},"required":["item_id","public_id","shipping_address","status"],"additionalProperties":false,"example":{"item_id":"01933b1c-2100-7000-8000-000000000070","public_id":"OI-ABC123","recipient_name":"Ana Pérez","recipient_email":"ana.perez@example.com","recipient_phone":"+34600111222","shipping_address":"Calle Mayor 12, 3º A\n28013 Madrid\nES","shipping_carrier":"Correos Express","tracking_code":"CE12345678ES","status":"shipped","shipped_at":"2026-05-28T10:15:00.000Z","delivered_at":null}},"StoreSession":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"token":{"type":"string"},"expires_at":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"customer_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"store_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},"required":["url","token","expires_at","customer_id","store_id"],"additionalProperties":false},"EngineEventResult":{"type":"object","properties":{"event_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"Unique id of this execution (time-ordered). THE key for support and audit lookups."},"status":{"type":"string","enum":["completed","skipped","failed"],"description":"Business outcome. `completed` applied the points. `skipped` = business rules declined (active window or a per-player velocity cap). `failed` = the execution ran and was recorded but did not apply. Business `failed` and `skipped` are terminal (a same-key retry replays the frozen verdict); only a `failed` with `error_code: internal_error` is retryable."},"replayed":{"type":"boolean","description":"True when the BUSINESS layer answered with the FROZEN result of a previous delivery with the same idempotency key (nothing was re-executed). Note: on `/engine/events` a same-key retry is usually short-circuited by the HTTP idempotency layer, which returns the ORIGINAL body verbatim (`replayed: false`) plus the `idempotent-replay: true` response header — detect replays via the header there."},"correlation_id":{"type":"string","description":"Tracing id tying together execution, audit trail and request log."},"customer_id":{"description":"Resolved player. Null when resolution failed.","anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},{"type":"null"}]},"points_requested":{"description":"Points value the pipeline decided to apply (override or default).","anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"points_applied":{"description":"Points actually written to the ledger. Null unless `completed`.","anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"balance_before":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"balance_after":{"description":"Wallet balance after the movement — from the ORIGINAL execution on replays, never a fresh read.","anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"error_code":{"description":"Machine-readable reason, present on `skipped`/`failed`. `skipped`: `outside_active_window`, `max_daily_reached` (per-player daily cap), `cooldown_active` (per-player min interval), `invalid_constraints`. `failed`: `player_id_missing`, `player_not_found`, `insufficient_balance`, `internal_error` (transient — retryable with the same key).","anyOf":[{"type":"string"},{"type":"null"}]},"error_message":{"anyOf":[{"type":"string"},{"type":"null"}]},"limit_context":{"description":"Present only on `status: skipped` with `error_code: budget_limit_reached` — the blocking budget policy's scope/metric/period and how much was requested vs remaining (spec sec 11).","anyOf":[{"type":"object","properties":{"scope":{"type":"string"},"metric":{"type":"string"},"period":{"type":"string"},"limit":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"consumed":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"requested":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"remaining":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"currency":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["scope","metric","period","limit","consumed","requested","remaining"],"additionalProperties":false},{"type":"null"}]}},"required":["event_id","status","replayed","correlation_id"],"additionalProperties":false,"description":"Result of one Engine event delivery. Deliveries that pass pre-flight always return 200 with this shape — check `status`, don't assume `completed`.","example":{"event_id":"01933b1c-2300-7000-8000-0000000000aa","status":"completed","replayed":false,"correlation_id":"corr_3f2a6d1e9c084b7fa1d2e3f4a5b6c7d8","customer_id":"01933b1c-1f00-7000-8000-000000000001","points_requested":25,"points_applied":25,"balance_before":100,"balance_after":125,"error_code":null,"error_message":null}}},"securitySchemes":{"BearerApiKey":{"type":"http","scheme":"bearer","bearerFormat":"tpp_<env>_<32 hex>","description":"Sub-brand-scoped bearer token. Format: `tpp_<env>_<32 hex chars>` where `<env>` is `live` or `test`. Each token declares a set of atomic scopes (`customers:read`, `orders:create`, ...) listed on each operation's `security` requirement."}}}}