E-commerce Flow
E-commerce Flow pattern for Node.js.
A canonical "buy a product" flow with @easylabs/node: customer + payment instrument captured from the frontend, charged via checkout, fulfilment metadata attached to the resulting order, refund issued on cancellation.
Goal
Single endpoint that turns a cart-and-card payload from the browser into a charged order, idempotently. The frontend tokenizes the card with the Easy Labs frontend SDK and posts only the resulting tokenId plus the cart — your server never touches PAN data.
Implementation
1. Create products + prices once
import { createClient } from "@easylabs/node";
const easy = await createClient({ apiKey: process.env.EASY_API_KEY! });
const product = await easy.createProduct({ name: "Notebook", active: true });
const price = await easy.createPrice({
product_id: product.data.id,
active: true,
recurring: false,
currency: "USD",
unit_amount: 2499,
tax_behavior: "exclusive",
});Persist price.data.id in your catalog table.
2. Charge endpoint
// POST /api/checkout
type Body = {
buyer: { first_name: string; last_name: string; email: string };
card: { tokenId: string; name: string };
cart: { price_id: string; quantity: number }[];
cart_id: string;
};
export async function checkout(body: Body) {
const result = await easy.checkout({
customer_creation: true,
customer_details: {
first_name: body.buyer.first_name,
last_name: body.buyer.last_name,
email: body.buyer.email,
},
source: {
type: "PAYMENT_CARD",
tokenId: body.card.tokenId,
name: body.card.name,
},
line_items: body.cart,
metadata: { cart_id: body.cart_id },
});
if (result.data.orderId) {
await easy.updateOrderTags(result.data.orderId, {
cart_id: body.cart_id,
fulfilment_state: "queued",
});
}
return result.data;
}3. Mark fulfilment complete
async function markShipped(orderId: string, trackingNumber: string) {
const { data: order } = await easy.getOrder(orderId);
await easy.updateOrderTags(orderId, {
...order.tags,
fulfilment_state: "shipped",
tracking_number: trackingNumber,
});
}4. Cancel + refund
async function cancelAndRefund(orderId: string, reason: string) {
const { data: order } = await easy.getOrder(orderId);
if (!order.transfer) throw new Error("Order has no captured transfer.");
await easy.createRefund(order.transfer.id, {
refund_amount: order.transfer.amount,
tags: { reason, source: "support_console" },
});
await easy.updateOrderTags(orderId, {
...order.tags,
fulfilment_state: "cancelled",
cancellation_reason: reason,
});
}Tradeoffs
checkoutdoes a lot. It creates the customer, instrument, order, and transfer in one call. If you need to insert validation between steps (KYC, fraud rules, address verification), callcreateCustomer→createPaymentInstrument→createTransferseparately and wrap them in your own state machine.- Idempotency.
checkoutdoesn't accept an idempotency key today. Dedupe at the application layer — storecart_idinmetadataand refuse a second submission that maps to the same cart. - Returning customers. Switch the call to
customer_creation: falseand pass the savedidentity_id+ a storedinstrument_id(string) so you charge a saved card without prompting again. - Subscriptions in the same cart. If the cart contains a recurring price,
result.data.subscriptionsis populated — link those subscription IDs back to your local user record. - Refund accumulation. Partial refunds add up against the original capture; the API returns 422 if you exceed it.