Create payments on your server
Every Stack payment starts on your server. Your server knows the price and holds your
secret key. It creates a payment intent and gives your app the intent’s
client_secret, which is all the Stack sheet needs.
Install
Section titled “Install”npm install @usestack/nodeNode 18 or later. @usestack/node has no dependencies.
import Stack from '@usestack/node';
const stack = new Stack(process.env.STACK_SECRET_KEY!);Create a payment
Section titled “Create a payment”import express from 'express';import Stack from '@usestack/node';
// Your secret key stays on your server. Never ship it in an app or a website.const stack = new Stack(process.env.STACK_SECRET_KEY!);const app = express();
// Your prices, in kobo. The app sends what the customer wants, never an amount.const PRICES: Record<string, number> = { jollof: 450_000, zobo: 120_000 };
app.post('/checkout', express.json(), async (req, res) => { const items: { id: string; qty: number }[] = req.body.items; const amount = items.reduce((sum, item) => sum + PRICES[item.id]! * item.qty, 0); const orderId = await createOrder(items, amount); // your own database
const intent = await stack.paymentIntents.create( { amount, // ₦5,700 is 570000 currency: 'NGN', description: 'Stack Food order', metadata: { orderId }, }, // Retrying this request never creates a second payment for the same order. { idempotencyKey: `order_${orderId}` }, );
res.json({ orderId, clientSecret: intent.client_secret });});
app.listen(4242);
declare function createOrder(items: { id: string; qty: number }[], amount: number): Promise<string>;| Parameter | Required | |
|---|---|---|
amount |
Yes | In kobo, as an integer. ₦24,000 is 2400000. At least 1. |
currency |
Yes | 'NGN' (the only currency for now). |
description |
No | Up to 500 characters. Shown to the customer in the sheet. |
metadata |
No | Any JSON object, returned on the intent and in webhooks. Put your order id here. |
customer |
No | { email }: who is paying, if you know. See Tell Stack who is paying. |
It returns the payment intent with its client_secret. Send the client_secret to your
app, and don’t log or store it. Everything else on the intent is safe to keep. See
Payment intents.
A payment intent can be paid for 30 minutes (expires_at). After that, create a new one.
Idempotency: never charge twice
Section titled “Idempotency: never charge twice”Networks fail, and your server may retry a request that actually went through. Pass an
idempotencyKey that is unique to the thing being paid for, such as the order:
await stack.paymentIntents.create({ amount, currency: 'NGN' }, { idempotencyKey: `order_${orderId}` });Sending the same key again returns the same payment intent instead of creating a second one. If you don’t pass a key, the SDK makes one per call, so its own retries are still safe.
Look up or cancel a payment
Section titled “Look up or cancel a payment”const intent = await stack.paymentIntents.retrieve('pi_...');if (intent.status === 'succeeded') { // paid}
// A payment the customer hasn't paid yet, e.g. the order was abandoned:await stack.paymentIntents.cancel('pi_...');Cancelling a payment that is already canceled just returns it. A payment that is
processing or succeeded can’t be cancelled: refund it instead.
Errors
Section titled “Errors”Every failed call throws a StackError:
import { StackError } from '@usestack/node';
try { await stack.paymentIntents.create({ amount: 0, currency: 'NGN' });} catch (err) { if (err instanceof StackError) { console.log(err.type); // 'invalid_request' console.log(err.status); // 400 console.log(err.message); // what went wrong, safe to log console.log(err.requestId); // quote this when you contact us }}A connection failure has type: 'network_error' and status: 0. See
Errors for every type.
Options
Section titled “Options”const stack = new Stack(process.env.STACK_SECRET_KEY!, { timeoutMs: 30_000, // per attempt (default 30 seconds) maxRetries: 2, // for network errors, 429 and 5xx (default 2)});Retries back off exponentially and always reuse the same idempotency key, so they can never create a second payment.
Not on Node? The REST API is plain JSON over HTTPS with your secret key as a Bearer token.