Skip to content

QRCode API

TokenFlight renders QR codes as <img> elements. By default, those images are served from the embed origin:

txt
https://embed.tokenflight.ai/api/qrcode
https://embed.tokenflight.ai/api/qrcode/transfer

You can point the widget at your own compatible QRCode API with qrCodeApiUrl.

html
<tokenflight-widget qr-code-api-url="https://example.com/api/qrcode"></tokenflight-widget>
ts
new TokenFlightWidget({
  container: "#widget",
  config: {
    qrCodeApiUrl: "https://example.com/api/qrcode",
  },
});

Relative paths are resolved against the embed config origin, not against apiEndpoint. Production uses https://embed.tokenflight.ai; staging uses https://embed-staging.tokenflight.ai.

Generic QR Endpoint

Use this endpoint for fiat payment links and other generic QR payloads.

txt
GET /api/qrcode?content=<content>&size=<size>
QueryRequiredDescription
contentYesURL-encoded text to encode directly into the QR code
sizeNoRequested SVG size in pixels

The endpoint must return an SVG image:

http
Content-Type: image/svg+xml; charset=utf-8
Access-Control-Allow-Origin: *
Cache-Control: no-store
X-Content-Type-Options: nosniff

Transfer QR Endpoint

Use this endpoint for ODA transfer tracking pages.

txt
GET /api/qrcode/transfer?amount=<decimal>&recipient=<address>&caip2=<caip2>&token=<token>&decimals=<decimals>&memo=<memo>&size=<size>
QueryRequiredDescription
amountYesHuman-readable decimal amount, for example 1.23
recipientYesDeposit address or payment recipient
caip2YesSource chain CAIP-2 identifier, for example eip155:8453
tokenYesSource token address or native token identifier
decimalsYesToken decimals used to convert display amount to base units
memoNoMemo, tag, or attribution value when the chain requires one
sizeNoRequested SVG size in pixels

The TokenFlight widget chooses a QR image source in this order:

  1. Use deposit.qrImageUrl directly when the transfer build response provides it.
  2. Encode deposit.paymentUri or deposit.qrPayload with the generic QR endpoint.
  3. Call <qrCodeApiUrl>/transfer when the build response only provides deposit details.
  4. Fall back to a generic QR for the raw recipient address when transfer params are incomplete.

Implementations should generate wallet-readable payment payloads when a chain has a reliable standard:

Chain familyRecommended payload
EVMEIP-681 URI for native or ERC20 transfers
BitcoinBIP21/BIP321-compatible bitcoin: URI
TronRecipient-only unless your provider intentionally supports a wallet-specific deeplink
CKBRecipient-only

If your API cannot safely generate a chain-aware payload, return a QR code for recipient only. Do not encode internal debug strings like address|amount|memo.

Default /transfer Rules

TokenFlight's default /api/qrcode/transfer implementation applies these rules:

RuleBehavior
Required paramsamount, recipient, caip2, token, and decimals must be present
Amount formatamount must be a decimal string such as 1 or 1.23
Decimalsdecimals must be an integer from 0 to 36
Sizesize is clamped to 96..512 pixels
Payload lengthfinal QR payloads over 2048 characters are rejected
Memoaccepted as a query param, but not embedded by default unless a chain-specific standard supports it
Fallbackunsupported or ambiguous chain/token inputs encode recipient only

For EVM source chains (caip2 namespace eip155):

  • Native tokens use:
txt
ethereum:<recipient>@<chainId>?value=<baseUnits>
  • ERC20 tokens use EIP-681 contract transfer format:
txt
ethereum:<tokenContract>@<chainId>/transfer?address=<recipient>&uint256=<baseUnits>
  • baseUnits is computed from amount and decimals.
  • If recipient is not an EVM address, or the ERC20 token is not an EVM address, the endpoint falls back to recipient only.
  • Native token identifiers currently include native, eth, 0x0000000000000000000000000000000000000000, and 0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee.

For Bitcoin source chains (caip2 namespace bip122):

txt
bitcoin:<recipient>?amount=<amount>

For Tron, CKB, Solana, and unknown chain namespaces, the default implementation returns a QR code for recipient only. This is intentional: wallet-specific deeplinks should only be added after the target wallet compatibility is confirmed.

Custom Provider Requirements

A custom provider must support both endpoints under the configured base URL:

txt
<qrCodeApiUrl>?content=...
<qrCodeApiUrl>/transfer?amount=...

For example, qrCodeApiUrl: "https://example.com/api/qrcode" means:

txt
https://example.com/api/qrcode?content=...
https://example.com/api/qrcode/transfer?amount=...

TokenFlight treats backend-provided qrImageUrl as the highest-priority source. When a transfer build response includes qrImageUrl, the widget uses it directly and does not call the configured QRCode API.

Example Custom Implementation

This example uses Cloudflare Workers and the qrcode package. It intentionally falls back to recipient-only QR codes for transfer chains it does not understand.

ts
import QRCode from "qrcode";

async function svgResponse(payload: string, size: number) {
  const svg = await QRCode.toString(payload, {
    type: "svg",
    width: Math.min(Math.max(size, 64), 512),
    margin: 1,
    errorCorrectionLevel: "M",
  });

  return new Response(svg, {
    headers: {
      "Content-Type": "image/svg+xml; charset=utf-8",
      "Access-Control-Allow-Origin": "*",
      "Cache-Control": "no-store",
      "X-Content-Type-Options": "nosniff",
    },
  });
}

export default {
  async fetch(request: Request) {
    const url = new URL(request.url);
    const size = Number(url.searchParams.get("size") ?? 168);

    if (url.pathname.endsWith("/transfer")) {
      const recipient = url.searchParams.get("recipient");
      if (!recipient) return new Response("Missing recipient", { status: 400 });

      const caip2 = url.searchParams.get("caip2") ?? "";
      const amount = url.searchParams.get("amount") ?? "";
      const payload = caip2.startsWith("bip122:")
        ? `bitcoin:${recipient}${amount ? `?amount=${amount}` : ""}`
        : recipient;

      return svgResponse(payload, size);
    }

    const content = url.searchParams.get("content");
    if (!content) return new Response("Missing content", { status: 400 });
    return svgResponse(content, size);
  },
};