QRCode API
TokenFlight renders QR codes as <img> elements. By default, those images are served from the embed origin:
https://embed.tokenflight.ai/api/qrcode
https://embed.tokenflight.ai/api/qrcode/transferYou can point the widget at your own compatible QRCode API with qrCodeApiUrl.
<tokenflight-widget qr-code-api-url="https://example.com/api/qrcode"></tokenflight-widget>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.
GET /api/qrcode?content=<content>&size=<size>| Query | Required | Description |
|---|---|---|
content | Yes | URL-encoded text to encode directly into the QR code |
size | No | Requested SVG size in pixels |
The endpoint must return an SVG image:
Content-Type: image/svg+xml; charset=utf-8
Access-Control-Allow-Origin: *
Cache-Control: no-store
X-Content-Type-Options: nosniffTransfer QR Endpoint
Use this endpoint for ODA transfer tracking pages.
GET /api/qrcode/transfer?amount=<decimal>&recipient=<address>&caip2=<caip2>&token=<token>&decimals=<decimals>&memo=<memo>&size=<size>| Query | Required | Description |
|---|---|---|
amount | Yes | Human-readable decimal amount, for example 1.23 |
recipient | Yes | Deposit address or payment recipient |
caip2 | Yes | Source chain CAIP-2 identifier, for example eip155:8453 |
token | Yes | Source token address or native token identifier |
decimals | Yes | Token decimals used to convert display amount to base units |
memo | No | Memo, tag, or attribution value when the chain requires one |
size | No | Requested SVG size in pixels |
The TokenFlight widget chooses a QR image source in this order:
- Use
deposit.qrImageUrldirectly when the transfer build response provides it. - Encode
deposit.paymentUriordeposit.qrPayloadwith the generic QR endpoint. - Call
<qrCodeApiUrl>/transferwhen the build response only provides deposit details. - 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 family | Recommended payload |
|---|---|
| EVM | EIP-681 URI for native or ERC20 transfers |
| Bitcoin | BIP21/BIP321-compatible bitcoin: URI |
| Tron | Recipient-only unless your provider intentionally supports a wallet-specific deeplink |
| CKB | Recipient-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:
| Rule | Behavior |
|---|---|
| Required params | amount, recipient, caip2, token, and decimals must be present |
| Amount format | amount must be a decimal string such as 1 or 1.23 |
| Decimals | decimals must be an integer from 0 to 36 |
| Size | size is clamped to 96..512 pixels |
| Payload length | final QR payloads over 2048 characters are rejected |
| Memo | accepted as a query param, but not embedded by default unless a chain-specific standard supports it |
| Fallback | unsupported or ambiguous chain/token inputs encode recipient only |
For EVM source chains (caip2 namespace eip155):
- Native tokens use:
ethereum:<recipient>@<chainId>?value=<baseUnits>- ERC20 tokens use EIP-681 contract transfer format:
ethereum:<tokenContract>@<chainId>/transfer?address=<recipient>&uint256=<baseUnits>baseUnitsis computed fromamountanddecimals.- If
recipientis not an EVM address, or the ERC20tokenis not an EVM address, the endpoint falls back torecipientonly. - Native token identifiers currently include
native,eth,0x0000000000000000000000000000000000000000, and0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee.
For Bitcoin source chains (caip2 namespace bip122):
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:
<qrCodeApiUrl>?content=...
<qrCodeApiUrl>/transfer?amount=...For example, qrCodeApiUrl: "https://example.com/api/qrcode" means:
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.
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);
},
};