Subscriptions & recurring billing
TowanPay is payment infrastructure — not a subscription product. Your app owns plans, schedules, and entitlements.
Same API, different pattern
There is no separate subscriptions endpoint. Each billing cycle creates a new order with metadata your webhook handler uses to extend access.
Architecture
Cron → POST /orders (metadata: subscription_id, plan, billing_period)
→ email paymentLinkUrl → customer pays
→ order.paid webhook → extend subscription in your DB
Renewal order example
Include subscription context in metadata — it is echoed in every webhook payload.
Monthly renewal
curl -s -X POST "https://pay.andgroupco.com/api/v1/projects/PROJECT_ID/orders" \
-H "Authorization: Bearer tp_test_xxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: quickstart-order-1" \
-d '{
"amountMinor": 9900,
"currency": "CNY",
"description": "Pro Plan — March 2026",
"buyerEmail": "customer@example.com",
"buyerName": "Jane Doe",
"metadata": {
"subscription_id": "sub_abc123",
"plan": "pro",
"billing_period": "2026-03"
}
}'Best practices
- Use
Idempotency-Key: renew-{sub_id}-{period}so retries never double-charge - Create a new order per cycle — payment links are one order, one amount, one token
- Handle
order.failedto notify subscribers and schedule retries - Store TowanPay
order.idon your invoice record for reconciliation
Webhook handler sketch
Pseudocode
async function handleOrderPaid(event) {
const { subscription_id, billing_period } = event.data.order.metadata;
if (!subscription_id) return;
await db.subscriptions.update({
where: { id: subscription_id },
data: {
status: "active",
currentPeriodEnd: nextPeriodEnd(billing_period),
lastOrderId: event.data.order.id,
},
});
}Do not reuse payment links
A payment link token is bound to one order. Reusing links across months causes reconciliation ambiguity. See payment links.
