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.failed to notify subscribers and schedule retries
  • Store TowanPay order.id on 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.