Skip to content

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 user
Browser ◀──account page──────┘ shows QR code + Add to Wallet buttons
  1. Create a Website API key (Getting started).

  2. Note the template id and the keys of the fields the form fills (e.g. member for the name).

  3. Put both into your server’s environment:

    Terminal window
    WALLETCRAFT_API_URL=https://api.walletcraft.app
    WALLETCRAFT_API_KEY=wc_…
    WALLETCRAFT_TEMPLATE_ID=…

Save this as walletcraft.mjs on your server. It needs Node.js 18 or newer and nothing else.

walletcraft.mjs
// 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`,
};
}
signup.mjs
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 }
}
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.