v1.0.0
OpenAPI 3.1.1

Phygitals IFrame API

The IFrame API embeds the full Phygitals pack-opening experience in your product. Players never leave your app and never create a Phygitals account — they play as a user of yours.

Phygitals never holds player funds. You stay the system of record for balances: every time a player spends or is paid, Phygitals calls your wallet server-to-server and acts on your answer.

How it works

The integration runs in two directions, and each is authenticated differently.

Direction Call Authenticated by
You → Phygitals launch — mint an iframe URL for one player Your X-API-Key
Phygitals → You Wallet operations RSA signature over the request body

Three steps to integrate:

  1. Call launch from your backend with the player's id. You get back a single-use URL.
  2. Embed it in an <iframe>. The player opens packs inside it.
  3. Expose your wallet — Phygitals calls it as the player spends, wins, keeps and gets refunded.

The wallet contract

Your wallet exposes five operations — four that change state, and one read.

Operation Money Purpose
debit out Charge the player — a pack, a shipping fee
credit in Pay the player — a card sold back
rollback reversed Undo a charge that shouldn't stand
recordAsset none Book a card the player kept
balance What the player has to spend

The paths and field names below are canonical, not mandatory. If your platform already exposes a seamless wallet — the iGaming bet / win / rollback surface, or anything like it — Phygitals writes the adapter that maps these onto your endpoints and you keep your wire. What you cannot change is what each operation means, and that is what this section specifies.

Common fields

Every call carries these. Anything your wire has no place for is simply not sent.

Field Type Description
user_id string Your player id, verbatim as you sent it to launch.
token string The player-session token from launch, if you sent one.
transaction_id string This call's id, minted by Phygitals. Unique. A repeat is a retry.
amount integer Minor units — 5800 is $58.00. Always positive; the operation names the direction.
reason string Human-readable context, e.g. pack:starter-pokemon x2. For your statements and support tooling.
reference_transaction_id string On rollback, the charge being undone.
game_code string Which registered wallet game this belongs to.

Every operation returns the player's resulting balance, in the same minor units:

{ "balance": 8805, "currency": "USD" }

balance

Read-only. The embedded UI shows this to the player and refreshes it after each transaction. Return their current spendable balance. Nothing here moves.

debit

Charge the player. Fires when they buy a pack (amount is the pack price × quantity) or pay a shipping fee to claim cards for physical delivery.

Refuse what you cannot cover. Insufficient funds is a normal answer, not an error — return your business refusal and Phygitals surfaces it to the player. Getting that distinction wrong is the one mistake that causes real accounting damage; see Your response decides what happens next below.

credit

Pay the player. Fires when they sell a card back (amount is the buyback price), or when a card is auto-sold because they left it undecided past the claim window — currently 1 hour after the pull.

A credit is only sent after the sale has committed on our side. The player is owed the money whatever happens to the request, which is why a failed credit is retried and a failed debit is not.

rollback

Reverse a charge that should not stand. Fires when the pack draw fails after the player was debited, when the shipment fails to book after the fee was debited, or when a debit's outcome was never learned and the charge may or may not exist.

A rollback carries two ids. transaction_id is the reversal's own id — it is a transaction in its own right, so a retry reusing it must not reverse twice. reference_transaction_id is the charge being undone.

A rollback for a charge you never took is expected, not exceptional. When we don't learn a debit's outcome we reverse it defensively. Return your "no such transaction" code and we treat it as nothing to undo.

recordAsset

No money moves. The player claimed a card for physical shipping, so it leaves the system and will never be sold back to them. This books the asset against the player: what they now hold, and what you owe Phygitals for it. Fires once per card, after the shipment is booked and the fee is charged. amount is what the card was worth — its buyback value at the moment it was claimed.

This operation is optional. If you have no asset concept, don't expose it — Phygitals sends nothing at all for a kept card.

Settlement rules

These rules govern every wallet call. Together they are what make the integration safe to retry.

Transaction ids

Every call carries a transaction_id minted by Phygitals and written to our own ledger before the request leaves. When a call is retried, the id is read back off that row — never regenerated.

That gives you one rule to implement: treat transaction_id as unique per player. A repeat is a retry, not a new transaction. Return the original result and do not apply it twice.

debit is the deliberate exception. A player retrying a failed purchase is making a new purchase, so it gets a new id and is a new charge. Phygitals never re-sends a debit.

Your response decides what happens next

This is the rule that matters most. Phygitals sorts every wallet response into three buckets:

Your response We read it as What we do next
Success, with the resulting balance applied Continue.
A business refusal you answered — insufficient funds, unknown transaction declined — definitively nothing moved Fail the player's action. No reversal.
Timeout, connection failure, 5xx, unparseable body unknown — it may have been applied Reverse a debit. Replay a credit.

The exact codes are mapped at onboarding — an HTTP 200 carrying a status field in the body works just as well as a 4xx with an error code. What matters is only that a refusal is distinguishable from a failure.

Three things not to do:

  • Don't return a transport-level failure for a business refusal. A 500 or a dropped connection for what is really "insufficient funds" turns a definite no into a maybe. Phygitals then sends a rollback for a charge you never took.
  • Don't return success for a call you did not apply. Success is read as money moved. The player is shown a balance you never wrote, and the gap is silent — nothing retries a call that reported success.
  • Don't hang. A wallet call that does not answer promptly is treated as unknown, with all the reversal traffic that implies. Refuse fast rather than block.

Retries

A failed credit, rollback or recordAsset is not dropped. Phygitals records it, retries it on a backoff, and keeps retrying until it lands or the attempt budget is exhausted — always with the same transaction_id.

Operation On failure
debit Never retried. An unknown outcome gets a rollback instead.
credit Retried — the player is already owed the money.
rollback Retried — the charge is already known to be void.
recordAsset Retried — the card has already shipped.

The asymmetry is the whole design: a debit's failure is safe to abandon (the player just didn't get their pack), while a credit's failure means somebody is out of pocket until it lands. Which is why idempotency isn't optional — every one of those retries is a byte-identical repeat of a call you may already have applied.

Amounts and currency

  • Amounts cross the wire as integers in your currency's minor units5800 is $58.00. Balances come back the same way.
  • They are always positive. The operation names the direction; nothing is ever negative.
  • If you scope players by currency, tell us how it's encoded on the player id and we send the matching currency on each transaction. Phygitals does not convert between currencies — we send whatever currency the player is in.

Authentication

You hold two credentials, one per direction.

Credential Direction You use it to
X-API-Key You → Phygitals Authenticate your launch call
RSA public key Phygitals → You Verify the signature on every wallet call

Launch key

A Phygitals-issued key, sent as X-API-Key on launch. Tied to your partner account and mode-tagged — separate keys for sandbox and live. Stored by Phygitals as a hash; the secret is shown once, at creation. Lost it? Revoke and mint a new one.

Call launch from your backend only. The key must never reach a browser, the embedded page, or your client bundle. The browser only ever sees the resulting launch_url.

Your player token

If you send token on launch, Phygitals stores it — encrypted, keyed to (partner, player, mode).

It has to outlive the iframe. A player can close the tab holding cards they never decided on; about an hour later Phygitals auto-sells those back and has to credit you for them, with no live session left to supply the token.

Scope Live sessions only. A sandbox launch stores nothing.
Retention Only while that player has something outstanding — undecided cards, or a wallet call still being retried.
Erasure Cleared as soon as nothing is outstanding and their session window has lapsed.
Handling Encrypted at rest, and redacted from logs.

Sending token is optional. If your wallet doesn't authorize per session, omit it and Phygitals stores nothing.

Verifying wallet calls

Every wallet call carries an X-API-Signature header: a base64 RSA-SHA256 signature over the exact request body, signed by Phygitals. Verify it with the public key we issue at onboarding, and reject anything that fails.

import { createVerify } from "node:crypto";

// `rawBody` must be the exact bytes received — capture it before
// any JSON body-parser middleware touches the stream.
function verify(rawBody: Buffer, signature: string): boolean {
  return createVerify("RSA-SHA256").update(rawBody).verify(PHYGITALS_PUBLIC_KEY, signature, "base64");
}

Verify against the raw request body bytes, not a re-serialized object. Decoding and re-encoding JSON can reorder keys or change spacing, and the signature will not match. Most frameworks discard the raw body by default — capture it first. A missing or invalid signature must be rejected with 401, and no funds may move.

The launch key authenticates you to us; the signature authenticates us to you. They are separate on purpose: a leaked launch key cannot be used to forge a wallet call, and a stolen wallet payload cannot be replayed into a session.

Embedding

Render the launch_url exactly as returned — the player origin is issued at onboarding and can change, so never reconstruct the URL or hardcode its host.

<iframe
  src="https://…/play?lt=plt_AbC…"
  allow="clipboard-write"
  style="border: 0; width: 100%; height: 800px;"
></iframe>

Allow-list your origins

The player app sends Content-Security-Policy: frame-ancestors, so only approved parents can embed it. Send us every origin you'll embed from, production and staging, and we'll add them. frame-ancestors takes origins — scheme plus host, no path, no trailing slash: https://app.yourbrand.com. An origin that isn't listed is blocked by the browser, not by us, and the failure shows as a CSP error in the console.

Game codes

game_code chooses which category the player opens on. Omit it and they land on the full catalog. That is all it does — the player can switch categories inside the iframe at any time, and the launched code does not restrict the catalog, the price, or anything else.

The codes are fixed — one per category:

game_code Category
phygitals_pokemon Pokémon
phygitals_one_piece One Piece
phygitals_baseball Baseball
phygitals_football Football
phygitals_basketball Basketball

Sandbox and live

Your launch key is mode-tagged, and the mode rides through the whole session.

Launch key Wallet calls go to Catalog
Sandbox Your stage environment Sandbox packs
Live Your production environment Live packs

Credentials are issued per environment. Integrate fully on sandbox — including a deliberately failed credit, so you see a retry arrive — before requesting live keys.

Server:https://api.phygitals.com

Production

Client Libraries

Launch

Mint a single-use iframe URL for one player. The only call you make to Phygitals — everything else in the integration is Phygitals calling your wallet.

Launch Operations

Launch an embedded player session

Mints a single-use URL for one player and returns it. Render that URL in an <iframe> and the player opens packs inside your product, without leaving it and without a Phygitals account.

Call this from your backend, authenticated with your X-API-Key — the same partner key the REST surface takes. The key's mode decides everything downstream: a sandbox key draws the sandbox catalog and settles against your stage wallet, a live key draws real inventory and settles against production.

user_id is the player's id in your system. Phygitals never resolves it to a Phygitals account; it is echoed verbatim on every wallet call so you can attribute the money. token is your own player-session token, replayed on those wallet calls for wallets that authorize per session — omit it if yours does not. game_code chooses which card category the player lands on; omit it for the full catalog.

Two clocks, and they are easy to confuse. The returned launch_url is single-use and expires in 60 seconds (expires_at), so mint it on demand immediately before rendering the iframe — never cache it or hand the same one to two tabs. The session it opens is the longer one: the player keeps playing for 45 minutes, and re-launching starts a fresh session rather than extending it.

The player app sends Content-Security-Policy: frame-ancestors, so every origin you embed from has to be allow-listed by Phygitals first. An origin that is not on the list is blocked by the browser, not by this endpoint.

Body
required
application/json
  • json
    Type: object
    required
Responses
  • application/json
Request Example for post/p3/rpc/launch
curl https://api.phygitals.com/p3/rpc/launch \
  --request POST \
  --header 'Content-Type: application/json' \
  --header 'X-API-Key: YOUR_SECRET_TOKEN' \
  --data '{
  "json": {
    "user_id": "",
    "token": "",
    "game_code": ""
  }
}'
{
  "json": {
    "launch_url": "string",
    "expires_at": "string"
  }
}