@agglabs-one/pay
v0.1.0
Published
Client SDK for AGG One Payments — signed integration API (customers, cards, invoices, subscriptions) and outbound webhook verification.
Maintainers
Readme
@agglabs-one/pay
Client SDK for AGG One Payments (pay.agglabs.com). Signed integration API +
outbound webhook verifier — one shared HMAC secret for both directions.
npm install @agglabs-one/payQuick start
import { Pay } from '@agglabs-one/pay';
const pay = new Pay({
keyId: process.env.PAYMENTS_KEY_ID!, // ak_...
keySecret: process.env.PAYMENTS_KEY_SECRET!,
// baseUrl defaults to https://pay.agglabs.com
});
// 1) Make sure we have a customer for this workspace
const customer = await pay.customers.ensure({
appId: 'agg-one',
externalId: workspaceId,
email: user.email,
name: workspace.name,
currency: 'PLN',
});
// 2) Create a monthly subscription with a 7-day trial
const { subscribeUrl, payUrl } = await pay.subscriptions.create({
customerId: customer.id,
currency: 'PLN',
description: 'Plan Pro',
unitAmount: 29.99, // major units per period
interval: 'month',
trialDays: 7,
proration: 'create_prorations',
});
// Send `payUrl` to redirect straight to Stripe Checkout,
// or `subscribeUrl` for the AGG-hosted status page.What's in the box
| Namespace | Highlights |
|---|---|
| pay.customers | ensure, get, invoices, charge (off-session) |
| pay.cards | setupIntent, list, makeDefault, remove |
| pay.invoices | create, receipt, markPaid, isPaid |
| pay.subscriptions | create, get, getByToken, listByCustomer, cancel, resume, changePrice, periods |
| pay.webhooks | test, recent (debugging) |
| pay.call(action, params) | Escape hatch for any integration action |
Incoming webhooks
Configure a URL on the payments side (env INTERNAL_WEBHOOKS), then verify
each delivery with verifyPayWebhook. The same secret used above unlocks it.
import express from 'express';
import { verifyPayWebhook, isPayEvent } from '@agglabs-one/pay';
const app = express();
app.post(
'/api/payments/webhook',
express.raw({ type: 'application/json' }), // raw body is required
(req, res) => {
let event;
try {
event = verifyPayWebhook({
secret: process.env.PAYMENTS_KEY_SECRET!,
rawBody: (req.body as Buffer).toString('utf8'),
headers: req.headers,
});
} catch (e) {
return res.status(401).end(); // bad signature — reject
}
if (isPayEvent(event, 'subscription.payment_succeeded')) {
const { subscriptionId, receiptUrl } = event.data;
// grant access / email the customer / etc
}
res.status(200).end(); // ack — retry stops
},
);Events emitted:
invoice.paid,invoice.past_due,invoice.voided,invoice.refundedsubscription.created,subscription.updated,subscription.canceledsubscription.payment_succeeded(withreceiptUrlfor the PDF)subscription.payment_failedcustomer.card_added,customer.card_removedwebhook.test(viapay.webhooks.test())
Retry schedule on the payments side: 30s → 5min → 30min → 3h → 12h, then
dead. Ack with any 2xx to stop retries.
Errors
Every failed call throws AggError (re-exported from @agglabs-one/core) —
err.code matches the payments service's error taxonomy (amount_below_min,
no_card_on_file, unauthorized, unknown_action, …), err.status is the
HTTP status. Also re-exported: InvalidKeyError, NotFoundError, ConflictError.
Design notes
- Amounts in minor units on the wire, major units on inputs where humans
care (
unitAmount: 29.99). Fields ending inCentsare always integers. - Same HMAC scheme both directions: signed string
"<unix>.POST.<path>.<sha256(body)>", hex HMAC-SHA256 with the shared secret. - No Stripe SDK on your side —
Payis the only import you need for the server flow. The frontend still uses Stripe.js with the payments service's publishable key to confirmclient_secrets returned bypay.cards.setupIntent().
