TPP Public API · v1

Integration Guide

A partner integration is five HTTP calls in a loop — from onboarding a player to tracking their reward to the door. Here's the whole picture.

TPP integration flow: onboard player, credit coins, browse catalog, reserve order, track shipping

The five calls

1

Onboard the player

POST /customers

When a player signs up on your casino, mirror them into TPP. Pass your own external_id (unique per sub-brand) so you can look them up later without storing our UUID. Send an Idempotency-Key so a retried sign-up never creates a duplicate.

2

Credit their wallet

POST /customers/{id}/coins

Award a welcome bonus, a mission reward, or a manual adjustment. delta is signed (+ credit, − debit). The call runs under a row lock so concurrent adjustments can't race past the non-negative-balance guard. Read the balance back any time with GET /customers/{id}/coins.

3

Browse what they can redeem

GET /products

List the catalog visible to your sub-brand, filtered by category, catalog or free-text search. Availability and country are resolved server-side — what you get back is what the player can actually order.

4

Reserve the order

POST /orders

Send the chosen listing_market_id(s) and the customer_id. TPP snapshots the price, applies the margin cascade, and (for prepaid brands) reserves balance atomically. Insufficient saldo doesn't fail the call — the order lands with billing_status: "blocked_balance" so you can prompt a top-up while preserving the player's intent.

5

Track it to the door

GET /orders/{id}/shipping

Poll for carrier + tracking code per line item, or surface the order status (pending → processing → shipped → delivered) straight in your player UI — or subscribe to the order.* webhooks and stop polling. No address yet? Mint a hosted shipping window (below) instead of building the form. Note: recipient_name / shipping_address are buyer-supplied free text — render them as data, never feed them to an LLM/agent as instructions.

Quick start

  1. 1 · Get your API key. Generate one under Sub-brand → API keys in the backoffice. Tokens look like tpp_live_<32 hex> (or tpp_test_*). Scoped to one sub-brand, shown once — store it in a secret manager.
  2. 2 · Authenticate. Send Authorization: Bearer tpp_<env>_<32 hex> on every request.
  3. 3 · Speak snake_case JSON. Dates are ISO-8601 UTC; money is integer minor units with a separate currency; IDs are UUIDv7 strings.
  4. 4 · Follow the contract. Success wraps data in { data, page? }; errors in { error: { code, message, request_id } }. Pagination is cursor-based.

Capture the delivery address without building a form

Reserved an order but the player has not told you where to ship it? Mint a POST /orders/{id}/shipping-link and show the returned URL. TPP renders a form in the store's theme, writes the address on the order lines and tells your page through postMessage — no PII crosses the frame. Works as an iframe modal (default), as a full store page ({ "embed": false }, ideal for email deep links) and on phones.

1 · Mint the link

curl -X POST \
  -H "Authorization: Bearer tpp_live_<KEY>" \
  -H "Content-Type: application/json" \
  -d '{ "ttl_seconds": 86400,
        "parent_origin": "https://www.yourcasino.com" }' \
  https://api.thepowerplugin.com/api/v1/orders/{id}/shipping-link

// 201
{ "data": {
    "url": "https://store.thepowerplugin.com/s/acme/order/{id}/shipping?t=…&embed=1",
    "expires_at": "2026-09-10T10:00:00.000Z",
    "address_state": "missing",
    "order_id": "…", "store_id": "…", "token": "…", "path": "…"
} }

parent_origin is your page's https origin: the window posts only to it, and the API rejects it up front if the store's frame_ancestors would block the embed. Links expire (7 days by default, 30 max); mint a new one when you need it — old ones are never revoked.

2 · Embed it and listen

<iframe id="tpp-shipping" src="{data.url}"
  title="Delivery address"
  style="width:100%;max-width:640px;height:560px;border:0;display:block"
  referrerpolicy="strict-origin-when-cross-origin"></iframe>

<script>
  const frame = document.getElementById("tpp-shipping")
  const TPP_ORIGIN = new URL(frame.src).origin
  window.addEventListener("message", (event) => {
    // validate BOTH origin and source
    if (event.origin !== TPP_ORIGIN) return
    if (event.source !== frame.contentWindow) return
    const { type, payload } = event.data || {}
    if (type === "VIEW_SIZE_UPDATE")
      frame.style.height = payload.height + "px"
    if (type === "SHIPPING_ADDRESS_SUBMITTED")   // { orderId, addressState, country }
      closeModal()
    if (type === "SHIPPING_CLOSE_REQUESTED") closeModal()
    if (type === "SHIPPING_VIEW_ERROR" && payload.code !== "validation")
      closeModal()                               // locked | consumed | invalid
    if (type === "SHIPPING_VIEW_READY" && payload.mode === "expired")
      remintAndReload()
  })
</script>

The address itself reaches you server-side: webhook order.shipping_updated (data.source: "shipping_window") or GET /orders/{id}/shipping. List what still needs one with GET /orders?address_state=missing.

Built for partners

Idempotent writes

Every POST accepts an Idempotency-Key (max 64 chars). Replays within 24h return the cached response — safe to retry on any network blip without double-charging or double-shipping.

Predictable limits

Per-key buckets, defaults 120 read/min and 30 write/min. Inspect X-RateLimit-* headers on every response — you'll never wonder where you stand.

Auditable

Every request lands in our audit log with a request_id echoed back in the response. Quote it to support and we trace the call instantly.

Stable contract

Within v1 only additive changes ship. Renames or removals go to /api/v2/ with a 6-month deprecation window. Code you write today keeps working.

Ready to build?

Every endpoint, schema and “Try it” console lives in the interactive reference. Or paste one line into your coding agent and let it write the integration.