FlipKey Redeem SDK

Drop-in JS + CSS for publisher redemption pages. Wallet-signed redemption flow with safe-by-default key handling — keys never enter the page DOM.

Architectural note. The SDK runs on your redemption page, on your domain. FlipKey itself never holds, transits, or sees the keys you issue. The widget collects a wallet signature and POSTs it to your backend; your backend verifies on-chain ownership, claims a key from your own inventory, and returns either a redirect URL, an entitlement-granted notice, or a short-lived show-key payload. The key never appears in HTML attributes.
v0.2.0 — breaking changes from v0.1.x. The widget no longer accepts data-key; that pattern leaked keys to extensions, session-replay tools, and DOM snapshots. New required attributes are data-token-id and data-redeem-endpoint. See the migration section below.

Quick start

<link rel="stylesheet" href="https://flipkey.gg/sdk/redeem/flipkey-redeem.css">
<div class="fk-redeem"
     data-platform="steam"
     data-token-id="42"
     data-redeem-endpoint="/api/redeem"
     data-publisher="Acme Games"></div>

<!-- Nothing else to load. The widget lazy-imports the FlipKey wallet
     connector itself, so buyers can sign in with a passkey (no wallet
     extension, no account setup). Add data-chain-id="8453" for a
     Base-mainnet drop; the default is Base Sepolia testnet (84532). -->
<script src="https://flipkey.gg/sdk/redeem/flipkey-redeem.js"></script>
<script>FlipKeyRedeem.mountAll();</script>

The widget renders a Sign in & Redeem button. On click it connects the user's wallet, asks for a signature on a domain-bound message that includes the token ID and a fresh nonce, then POSTs { tokenId, wallet, signature, nonce, issuedAt } to data-redeem-endpoint.

Base Account is the default (passkey/email/social, the wallet FlipKey buyers have). If the buyer has a browser-extension wallet, a small "Use a browser wallet (Coinbase, MetaMask, …)" link appears beneath the button as a fallback.

Set data-platform to one of: steam, xbox, playstation, nintendo, epic. data-publisher is optional and is rendered in the widget heading.

Identity gate (Steam & Epic)

When data-platform is steam or epic, the widget adds an identity-confirmation step before the wallet flow. On mount it calls GET {base}/api/{platform}/me (same origin as your redeem endpoint). If the buyer isn't signed in, the widget renders a "Sign in with Steam/Epic" screen pointing at {base}/api/{platform}/auth; after the OAuth/OpenID round-trip the buyer returns to the page authenticated. The confirmed account id then rides along in the redeem POST as steamId / epicAccountId.

xbox, playstation, and nintendo have no public OAuth, so they skip the gate entirely — the widget goes straight to wallet connect.

Endpoints your backend must expose for a gated platform (relative to the redeem endpoint's origin):

EndpointMethodReturns / does
/api/{platform}/meGETProfile (steamId/accountId, display name, avatar) if signed in; 401 or empty otherwise. Reads a same-origin session cookie — the widget sends credentials: 'include'.
/api/{platform}/authGETStarts the OAuth/OpenID redirect and returns the buyer to the redeem page authenticated.
/api/{platform}/logoutPOSTClears the identity session (powers the "use a different account" link).

Why gate at all? For Steam, keys redeem to whichever account is logged into the browser — confirming first prevents activating on the wrong account. For both platforms, a verified platform identity is your input for refund-eligibility and bad-actor screening.

Live demo

Live widget rendered with v0.2.0 against a stub demo backend that returns a synthetic DEMO-… key. Click the button to walk through the flow — wallet connect, signature, response handoff. (Requires a browser wallet.)

Demo — Steam (redirect path)

Backend handler contract

Your data-redeem-endpoint receives:

{
  "tokenId": "42",
  "wallet": "0xabc...",
  "signature": "0x...",
  "nonce": "deadbeef...",
  "issuedAt": 1730000000000,
  "contract": "0x...",          // present only for multi-contract publishers
  "steamId": "7656119..."       // OR "epicAccountId" — present only on gated platforms
}

Your handler must:

  1. Verify signature was produced by wallet over the SDK's message format, using a smart-wallet-aware verifier — viem's action-level verifyMessage (handles EOA, ERC-1271, and ERC-6492). An ecrecover-only check (ethers.verifyMessage) rejects Base Account, the SDK's default smart-contract wallet. See examples/redeem-handler.js.
  2. Verify nonce is fresh and within ~5 minutes of issuedAt — replay protection.
  3. On gated platforms (Steam/Epic), confirm steamId/epicAccountId is present and record it — it is the second factor and your input for refund-eligibility and bad-actor screening.
  4. Verify on-chain that wallet owns tokenId in your FlipKey-deployed contract.
  5. Atomically claim a key from your inventory (UPDATE keys SET status='redeemed' WHERE status='unredeemed' LIMIT 1).
  6. Report the activation back to FlipKey, signed with your webhook_secret.
  7. Set Cache-Control: no-store on the response and return one of the three result shapes:
The signed message includes your publisher name. The SDK signs data-publisher as part of the message, so your handler must rebuild the message with the exact same publisher string or signature verification will fail.
{ "result": "redirect", "url": "https://store.steampowered.com/account/registerkey?key=..." }
{ "result": "entitlement_granted" }
{ "result": "show_key", "key": "XXXXX-XXXXX-XXXXX", "redeemUrl": "https://...", "expiresInSec": 90 }

Use redirect for platforms that accept pre-fill (Steam, PSN). Use entitlement_granted if you used a direct platform API to grant the game without exposing a key (Steamworks GrantPackage, etc.) — preferred when available. Use show_key only for platforms that don't support pre-fill (Xbox, Nintendo, Epic).

Since v0.5.0 the widget does not auto-navigate on a redirect result — it shows the buyer a button to open the pre-filled URL. This is deliberate: auto-redirecting mid-flow could trigger a platform re-login that swallowed the key before the buyer was ready.

Link-test handler

Before FlipKey lets you list a game, every redemption URL on the listing must pass a live link test. FlipKey calls your URL with ?fk_test=1&nonce=<random> and expects:

{ "nonce": "<echoed>", "sig": "<hex hmac_sha256(your_webhook_secret, nonce)>" }

This proves the URL exists, runs your handler, and that you control the FlipKey-issued webhook_secret. A drop-in implementation lives in examples/link-test-handler.js.

Per-platform notes

PlatformDeliveryCode formatNotes
steamRedirect (pre-fill)5x5 with hyphensCleanest UX; ?key= attaches the code. Direct entitlement also possible via Steamworks GrantPackage
xboxShow-key (paste)25-char with hyphensMicrosoft's redeem.microsoft.com doesn't accept pre-fill
playstationRedirect (pre-fill)12-digit (often 4-4-4)Sony's ?voucherCode= opens the redemption modal pre-filled
nintendoShow-key (paste)16-digitAccount must have accessed Switch eShop at least once
epicShow-key (paste)variesEpic's pre-fill OAuth is a separate gated integration

Security model

The SDK enforces these properties; your backend handler is responsible for the rest.

What the SDK guarantees

What you (the publisher) are responsible for

Migrating from v0.1.x

v0.1.x took the key as data-key on the widget element and rendered it directly. That pattern is unsafe and has been removed in v0.2.0. If you are on v0.1.x, you must:

  1. Update the widget HTML to remove data-key and add data-token-id + data-redeem-endpoint.
  2. Implement a backend handler at the data-redeem-endpoint URL that follows the contract above.
  3. Implement the link-test handler so your redemption URL can pass FlipKey's listing-time verification.
  4. Remove any server-side template logic that injects the key into the page HTML.
  5. Add Cache-Control: no-store on both the redemption page response and the redeem JSON endpoint.

The reference handlers in examples/ are the fastest path; for most publishers it's a one-day port.

Programmatic API

FlipKeyRedeem.mount(document.querySelector('#my-redeem-widget'));
FlipKeyRedeem.mountAll('.fk-redeem'); // selector optional, defaults to '.fk-redeem'
FlipKeyRedeem.PLATFORMS;              // platform metadata
FlipKeyRedeem.version;                // current SDK version

Reference templates

Per-platform reference pages using the SDK against a stub demo backend (returns synthetic DEMO-… keys, useful for verifying the flow):

Versioning

Current: 0.6.0. The SDK is pre-1.0 — APIs may change. Pin the version in your script tag once 1.0 ships.

Changelog

License

MIT. Source at /sdk/redeem/.