Sandbox

Go to production
PackflipPartner

Building with a coding agent

llms.txt, a starter prompt, and integration rules.

These docs are written to be read by coding agents such as Claude Code, Cursor, or Codex, as well as by people.

Machine-readable resources

ResourceURLUse it for
llms.txt/llms.txtAn index of every guide, with links.
llms-full.txt/llms-full.txtAll guides in one Markdown file, for a single fetch.
OpenAPI 3.1/openapi.jsonExact request and response schemas. Generate types or a client from it.
Guides as Markdown/docs/{guide}.md, for example /docs/quickstart.mdFetching one guide without HTML.

Both hosts serve the same documents; the prompt below reads them from the sandbox. If your agent's browser cannot open text/markdown, use llms-full.txt or the HTML pages at /docs/{guide}.

A prompt to start with

Paste this into your agent, and adjust the first paragraph to your product:

Integrate the Packflip Partner API into this project so users can buy card packs,
see their cards, and sell cards back.

Read https://sandbox.partner.packflip.xyz/llms-full.txt first, and use
https://sandbox.partner.packflip.xyz/openapi.json as the source of truth for
request and response shapes. Follow the "Off-chain lifecycle" guide in it for
the purchase flow.

Requirements:
- Call the API only from server code. Read PACKFLIP_BASE_URL, PACKFLIP_API_KEY,
  and PACKFLIP_WEBHOOK_SECRET from the environment. Default the base URL to
  https://sandbox.partner.packflip.xyz.
- Create one Packflip customer per user, with externalUserId set to our user ID,
  and store the returned cus_ ID. On 409, look the customer up by external ID.
- Send an Idempotency-Key on every POST /api/v2/operations. Derive it from a
  database record created before the call, and reuse it on retries.
- Treat money as decimal strings. Never convert to floating point.
- Add a webhook route that verifies webhook-signature against the raw body,
  deduplicates on the event id, and refreshes state from the API.
- Branch on error.code, not on the HTTP status: 409 can be
  insufficient_balance, out_of_stock, conflict, or idempotency_conflict.
- Offer off-chain buyback and redemption only for cards whose status is active,
  sealed is false, and onchain is null. Re-fetch cards after every operation.
- Write tests against the sandbox or with recorded responses; do not call
  production from tests.

Rules an integration must follow

  1. Keys stay on the server. An API key controls your whole balance.
  2. One environment per configuration. A base URL, key, and webhook secret always belong together. Sandbox is https://sandbox.partner.packflip.xyz; production is https://partner.packflip.xyz.
  3. Idempotency on every operation. Retries must reuse the key; new purchases must use a new one.
  4. Decimal money. Amounts are strings such as "1.500000".
  5. Strict requests, tolerant responses. Unknown request fields are rejected; unknown response fields must be ignored.
  6. Webhooks are hints. Verify, deduplicate, and re-read the resource.
  7. On-chain transactions come from the user's wallet. The API returns calldata; it never takes custody of user keys.

A minimal client

// packflip.ts: server-only
const baseUrl = process.env.PACKFLIP_BASE_URL ?? "https://sandbox.partner.packflip.xyz";
const apiKey = process.env.PACKFLIP_API_KEY!;

export class PackflipError extends Error {
  constructor(readonly status: number, readonly code: string, message: string) {
    super(message);
  }
}

export async function packflip<T>(
  path: string,
  init: { method?: string; body?: unknown; idempotencyKey?: string } = {},
): Promise<T> {
  const response = await fetch(`${baseUrl}/api/v2${path}`, {
    method: init.method ?? (init.body ? "POST" : "GET"),
    headers: {
      Authorization: `Bearer ${apiKey}`,
      ...(init.body ? { "Content-Type": "application/json" } : {}),
      ...(init.idempotencyKey ? { "Idempotency-Key": init.idempotencyKey } : {}),
    },
    body: init.body ? JSON.stringify(init.body) : undefined,
    cache: "no-store",
  });
  if (response.status === 204) return undefined as T;
  const json = await response.json();
  if (!response.ok) throw new PackflipError(response.status, json.error?.code, json.error?.message);
  return json as T;
}

// Example: draw one pack for a customer.
export function drawPack(customerId: string, vendingMachineId: number, orderId: string) {
  return packflip<{ id: string; status: string; cards: unknown[]; replayed: boolean }>("/operations", {
    body: { kind: "order", customerId, vendingMachineId, quantity: 1 },
    idempotencyKey: `order-${orderId}`,
  });
}

For typed responses, generate types from the OpenAPI document, for example with npx openapi-typescript https://sandbox.partner.packflip.xyz/openapi.json -o packflip.d.ts. For a complete purchase flow with retries, see Off-chain lifecycle.

Guides