@usestack/node
v0.1.1
Published
Stack for Node: create payment intents and refunds, verify webhooks, and work out fees. Server-side, with your secret key.
Maintainers
Readme
@usestack/node
The Stack server SDK: create payment intents, refund them, and verify webhooks. Your secret key lives here, on your server, and nowhere else.
pnpm add @usestack/nodeCreate an intent, send its client secret to your app
import Stack from '@usestack/node';
const stack = new Stack(process.env.STACK_SECRET_KEY!); // sk_test_… / sk_live_…
const intent = await stack.paymentIntents.create(
{
amount: 2_400_000,
currency: 'NGN',
description: 'Chicken rice bowl',
metadata: { orderId },
customer: { email: user.email }, // optional: the sheet goes straight to "enter the code"
},
{ idempotencyKey: `order_${orderId}` },
);
return { clientSecret: intent.client_secret };Amounts are minor units (kobo): ₦24,000 is 2400000. Every write carries an
idempotency key (generated if you don't pass one), so the SDK can retry network errors,
429s and 5xx safely, without charging twice.
Fulfil on the webhook, not on the app's word
// NestJS: NestFactory.create(AppModule, { rawBody: true }). Express: express.raw({ type: 'application/json' }).
const event = stack.webhooks.verify(req.rawBody, req.headers['stack-signature'], process.env.STACK_WEBHOOK_SECRET!);
if (event.type === 'payment_intent.succeeded') await orders.fulfil(event.data.metadata.orderId);verify throws StackSignatureError for a bad signature or anything older than five
minutes (replays). Pass the raw body: re-serialised JSON won't match.
| Event | When | data |
|---|---|---|
| payment_intent.succeeded | Money moved | id, amount, currency, method, stack_fee, metadata |
| payment_intent.failed | An attempt was declined; the intent stays payable | id, method, last_error, retryable |
Fees, and passing them on
stack.fees.of(1_000_000, 'card'); // ₦10,000 by card: Stack ₦50 + Paystack ₦250, you receive ₦9,700
stack.fees.grossUp(1_000_000, 'card'); // charge ₦10,306.13 to receive the full ₦10,000No network call. Stack's fee is exact; Paystack's is estimated from its published rates
({ international: true } for a foreign card). See the docs on passing fees on to customers.
Everything else
await stack.paymentIntents.retrieve('pi_…');
await stack.paymentIntents.cancel('pi_…');
await stack.refunds.create({ paymentIntent: 'pi_…', amount: 50_000 }); // omit amount for a full refund
stack.webhooks.generateTestHeader(body, secret); // for your own testsErrors are StackError with type, status, message and the API's requestId
(quote it to support). Options: new Stack(key, { baseUrl, timeoutMs, maxRetries }).
