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,
429and5xxare retried with backoff, honouringretry-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/nodeThe 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:
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');