Easy Labs
SDKsNode.jsExamples

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

  • checkout does 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), call createCustomercreatePaymentInstrumentcreateTransfer separately and wrap them in your own state machine.
  • Idempotency. checkout doesn't accept an idempotency key today. Dedupe at the application layer — store cart_id in metadata and refuse a second submission that maps to the same cart.
  • Returning customers. Switch the call to customer_creation: false and pass the saved identity_id + a stored instrument_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.subscriptions is 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.

On this page