Utopia PaymentsDocs

Node.js

Install the official Node.js and TypeScript library for the Utopia Payments API.

  • Zero dependencies, ESM and CommonJS, full TypeScript types
  • Every create sends an Idempotency-Key, so retries never double-charge
  • Network errors, 429 and 5xx are retried with backoff, honouring retry-after
  • Paginated lists walk themselves with for await
  • webhooks.unwrap() verifies Standard Webhooks signatures

Requires Node.js 20.3 or later.

Install

npm install @utopia-payments/node

The package is published from the public Utopia-Payments/utopia-node repository.

Quick start

Create a secret key in Dashboard → Developers and keep it on your server, for example in UTOPIA_API_KEY.

import Utopia from '@utopia-payments/node';

const utopia = new Utopia({ apiKey: process.env.UTOPIA_API_KEY });

// One-time payment: send the customer to the hosted checkout.
const session = await utopia.checkoutSessions.create({
  product_cart: [{ product_id: 'pdt_…', quantity: 1 }],
  customer: { email: 'customer@example.com' },
  return_url: 'https://your-store.com/thank-you',
  metadata: { order_id: '1001' },
});
// checkout_url is null once a session is complete or expired, never on a new one.
redirect(session.checkout_url!);

Amounts are integer minor units: 34900 is AED 349.00. See Currencies and amounts.

Subscriptions

const plan = await utopia.products.create({
  name: 'Pro',
  price: 9900,
  currency: 'AED',
  billing: 'recurring',
  billing_interval: 'month',
});

const session = await utopia.checkoutSessions.create({
  product_cart: [{ product_id: plan.product_id }],
  customer: { email: 'member@example.com' },
  return_url: 'https://your-app.com/billing',
});

// Later
await utopia.subscriptions.cancel('sub_…', { atPeriodEnd: true });

Webhooks

Verify every delivery against the raw request body:

Next.js route handler
export async function POST(request: Request) {
  const event = utopia.webhooks.unwrap(
    await request.text(),
    request.headers,
    process.env.UTOPIA_WEBHOOK_SECRET!,
  );
  // …
  return new Response(null, { status: 204 });
}

See Webhooks for an Express example and the events.

Webhooks can also be used without an API key when an application only needs to verify deliveries:

import { Webhooks } from '@utopia-payments/node';

const event = new Webhooks().unwrap(rawBody, headers, process.env.UTOPIA_WEBHOOK_SECRET!);

unwrap() throws WebhookVerificationError unless the signature matches and webhook-timestamp is within 300 seconds of now. Widen or narrow that window with the fourth argument — { tolerance: 600 } — if your receiver's clock drifts; a smaller number leaves less room for a replayed delivery.

Resources

The client exposes account, products, customers, checkoutSessions, payments, subscriptions, webhookEndpoints and events. webhooks is the key-free signature verifier.

Pagination

payments, products, customers, subscriptions and events return a PagePromise: await it for one page, or iterate it to walk every page.

const page = await utopia.payments.list({ limit: 50, status: 'succeeded' });

for await (const payment of utopia.payments.list({ limit: 100 })) {
  console.log(payment.payment_id, payment.total_amount);
}

webhookEndpoints.list() is the exception. Endpoints are not paginated, so it returns a plain Promise<Page<WebhookEndpoint>> with every endpoint of the key's mode in items, has_more: false and next_cursor: null. Await it; it takes no limit or cursor and cannot be iterated with for await.

const { items } = await utopia.webhookEndpoints.list();

Errors

import { NotFoundError, UtopiaError } from '@utopia-payments/node';

try {
  await utopia.payments.retrieve('pay_…');
} catch (error) {
  if (error instanceof NotFoundError) {
    // …
  } else if (error instanceof UtopiaError) {
    console.log(error.status, error.code, error.message);
  }
}

Error classes: InvalidRequestError, AuthenticationError, PermissionDeniedError, NotFoundError, ConflictError, RateLimitError, APIError, APIConnectionError, WebhookVerificationError.

Every one carries code, status and headers, the response's own Headers. That is where retry-after lives on a 429, along with x-ratelimit-remaining and x-ratelimit-reset:

import { RateLimitError } from '@utopia-payments/node';

try {
  await utopia.payments.list({ limit: 100 });
} catch (error) {
  if (error instanceof RateLimitError) {
    const seconds = Number(error.headers?.get('retry-after') ?? 60);
    // …
  }
}

You rarely need it: the client already waits for retry-after and retries 429, 5xx, network failures and IDEMPOTENCY_KEY_IN_USE up to maxRetries times. An error reaches you once those attempts are used up.

TypeScript

Every resource, parameter bag and option set is exported, so you can annotate your own functions without redeclaring the API:

import type { Payment, Subscription, CheckoutSessionCreateParams, WebhookEvent } from '@utopia-payments/node';

function receipt(payment: Payment): string {
  return `${payment.payment_id}: ${payment.total_amount}`;
}

Resources: Account, Product, Customer, CheckoutSession, Payment, Subscription, WebhookEndpoint, Event. Parameters: ProductCreateParams, ProductUpdateParams, ProductListParams, CustomerCreateParams, CustomerUpdateParams, CustomerListParams, CheckoutSessionCreateParams, ProductCartItem, PaymentListParams, SubscriptionListParams, SubscriptionUpdateParams, WebhookEndpointCreateParams, WebhookEndpointUpdateParams, ListParams. Unions and helpers: Currency, Metadata, BillingInterval, PaymentStatus, SubscriptionStatus, EventType, WebhookEvent, Page<T>, PagePromise<T>, UtopiaOptions, RequestOptions, and VERSION.

WebhookEvent is a discriminated union, so narrowing on event.type narrows event.data to a Payment or a Subscription for you.

Options

new Utopia({
  apiKey: 'sk_live_…',
  timeout: 30_000, // ms per request
  maxRetries: 2,
  baseUrl: 'https://utopia-payments.com/api/v1',
});

utopia.livemode tells you whether a client uses a live or a test key.

CommonJS is supported by the prepared package as well:

const { Utopia, Webhooks } = require('@utopia-payments/node');

On this page