2. Sign-up: issue a card
Your website keeps its own users and sign-up. When a user signs up, your server asks WalletCraft for their card and stores the card id with the user. One request does it: WalletCraft finds the customer by phone (or email) or creates them, issues the card and records the consents.
Browser ──sign-up form──▶ Your server ──POST /templates/{id}/passes──▶ WalletCraft │ stores card.id with the userBrowser ◀──account page──────┘ shows QR code + Add to Wallet buttonsBefore you start
Section titled “Before you start”-
Create a Website API key (Getting started).
-
Note the template id and the keys of the fields the form fills (e.g.
memberfor the name). -
Put both into your server’s environment:
Terminal window WALLETCRAFT_API_URL=https://api.walletcraft.appWALLETCRAFT_API_KEY=wc_…WALLETCRAFT_TEMPLATE_ID=…
The client
Section titled “The client”Save this as walletcraft.mjs on your server. It needs Node.js 18 or newer and nothing else.
// walletcraft.mjs — a tiny WalletCraft client for your website's backend (Node.js 18+, no dependencies).// Keep the API key on the server: never send it to the browser.const API_URL = (process.env.WALLETCRAFT_API_URL ?? 'https://api.walletcraft.app').replace( /\/+$/, '',);const API_KEY = process.env.WALLETCRAFT_API_KEY; // a "Website" key from the console (API & webhooks)
export class WalletCraftError extends Error { constructor(status, problem) { super(problem?.detail ?? problem?.title ?? `WalletCraft API error ${status}`); this.status = status; this.problem = problem; // RFC 9457 problem details: { type, title, status, detail, … } }}
async function call(method, path, body, headers = {}) { const res = await fetch(`${API_URL}/api/v1${path}`, { method, headers: { authorization: `Bearer ${API_KEY}`, ...(body === undefined ? {} : { 'content-type': 'application/json' }), ...headers, }, body: body === undefined ? undefined : JSON.stringify(body), }); const data = res.status === 204 ? null : await res.json().catch(() => null); if (!res.ok) throw new WalletCraftError(res.status, data); return data;}
/** * Sign-up: issues the user's card and records their consents. Safe to retry: the same `userId` always returns * the same card (Idempotency-Key, kept 24 hours). Store `card.id` with the user. */export function issueCard({ templateId, userId, name, phone, email, fieldValues = {}, privacyVersion, marketing = false, marketingVersion,}) { return call( 'POST', `/templates/${templateId}/passes`, { fieldValues, customer: { ...(phone ? { phone } : {}), ...(email ? { email } : {}), ...(name ? { name } : {}), consents: { privacy: { version: privacyVersion }, ...(marketing ? { marketing: marketingVersion ? { version: marketingVersion } : {} } : {}), }, }, }, { 'idempotency-key': `signup-${userId}` }, );}
/** The card: `publicUrl`, `status`, `loyaltyBalance`, `loyaltyTier`, `fieldValues`, … */export const getCard = (cardId) => call('GET', `/passes/${cardId}`);
/** Purchases, redemptions and other operations on the card, newest first. */export const getHistory = (cardId, limit = 20) => call('GET', `/passes/${cardId}/operations?limit=${limit}`);
/** News & offers consent of the card holder: `{ customer, marketingConsent }`. */export const getConsent = (cardId) => call('GET', `/passes/${cardId}/consent`);export const setConsent = (cardId, marketing, version) => call('PUT', `/passes/${cardId}/consent`, { marketing, ...(version ? { version } : {}) });
/** Links for the account page: the card page (QR code target) and direct "Add to Wallet" buttons. */export function walletLinks(card) { return { page: card.publicUrl, apple: `${card.publicUrl}/pass.pkpass`, google: `${card.publicUrl}/google`, appleBadge: `${API_URL}/assets/add-to-apple-wallet.svg`, googleBadge: `${API_URL}/assets/add-to-google-wallet.svg`, };}Issue the card on sign-up
Section titled “Issue the card on sign-up”import { issueCard } from './walletcraft.mjs';
// After your own user record is created:const card = await issueCard({ templateId: process.env.WALLETCRAFT_TEMPLATE_ID, userId: user.id, // your id: retries return the same card name: user.name, phone: user.phone, // E.164 or local format; phone identifies a customer first email: user.email, fieldValues: { member: user.name }, // values for the template's fields privacyVersion: 'privacy-2026-10', // the version of the privacy text the user accepted marketing: user.wantsNews, // only if they ticked "Send me news and offers"});
await db.users.update(user.id, { walletcraftCardId: card.id });The response is the card:
{ "id": "01a1…", "publicUrl": "https://api.walletcraft.app/p/Xy7…", "status": "active", "customerId": "01a1…", "loyaltyBalance": 0, "loyaltyTier": null, "issuedVia": "website", "fieldValues": { "member": "Julia Nowak", "points": 0 }}Common answers
Section titled “Common answers”| Status | Why | What to do |
|---|---|---|
| 201 | The card was issued (or replayed for the same userId). |
Store card.id. |
| 422 | A required field value is missing, or a phone/email is bad. | Show the detail to the user or fix the form. |
| 404 | The template id is wrong or archived. | Check WALLETCRAFT_TEMPLATE_ID. |
| 5xx | Temporary. | Retry with the same userId — no double cards. |