Webhooks, Subscriptions, and Credits
Understand durable fulfillment, monthly credit grants, idempotency, and the manual refund workflow.
VibeAny treats signed webhooks and database state as the source of truth for billing. Checkout return pages are user feedback only.
Durable webhook pipeline
All payment providers deliver to the same route:
POST /api/payments/webhookThe active provider verifier processes the delivery in this order:
- read the unmodified request body;
- verify the active provider's signature;
- normalize the provider event into application billing events;
- claim a webhook inbox entry by provider plus event ID or payload hash;
- apply subscription or credit fulfillment;
- mark the inbox entry processed only after fulfillment succeeds.
A duplicate delivery returns success with deduped: true. If fulfillment fails, the inbox claim is released so the provider can retry. Invalid signatures return HTTP 400; incomplete provider configuration returns HTTP 503.
Because webhook verification is also single-provider, a webhook from the inactive provider is not routed to its verifier. This is why changing PAYMENTS_PROVIDER is not a migration strategy.
Subscription state
Subscription webhooks store:
- the provider and external subscription/customer IDs;
- the plan and billing interval;
- lifecycle status and cancellation-at-period-end state;
- current period boundaries;
- provider event time and the next scheduled credit grant.
Writes are serialized per user, and older provider events do not overwrite newer state. The database enforces one subscription row per user.
Successful invoice events record the payment lifecycle but do not directly grant credits. Failed invoice handling is currently a no-op; dunning, access restriction, and customer notification are product policy that you must implement before promising them.
Independent monthly subscription credits
Subscription credits are granted on an application-owned monthly schedule rather than on every invoice webhook. This keeps annual subscriptions on monthly credit allotments.
Vercel calls the protected scheduler daily:
GET /api/cron/subscription-credits
Authorization: Bearer <CRON_SECRET>The included schedule is 5 0 * * *, or 00:05 UTC each day. Configure a strong server-only CRON_SECRET whenever subscription products are enabled.
For each active, trialing, or scheduled-for-cancellation subscription, the scheduler:
- uses the subscription period start as the monthly anchor;
- creates an idempotent
subscription_grantcredit lot; - expires that lot at the next monthly grant or the subscription period end, whichever comes first;
- advances the next-grant timestamp with a compare-and-set update.
Webhook processing also runs the due-grant use case, so a newly activated subscription can receive a due initial grant without waiting for the next daily cron.
Credit ledger behavior
Credits are consumed from the oldest unexpired lot first. The ledger keeps a cached balance and a FIFO projection, while source identifiers prevent the same payment or scheduled grant from being credited twice.
Built-in credit packs add:
| SKU | Credits | Expiration |
|---|---|---|
credits_small | 100 | 365 days after fulfillment |
credits_medium | 500 | 365 days after fulfillment |
credits_large | 1,500 | 365 days after fulfillment |
AI image generation costs 10 credits per image. It deducts credits before calling the model provider and adds them back if provider generation fails.
The Free plan's catalog copy is not an automatic recurring-grant job. If your product promises recurring free credits, add and test an explicit scheduler or onboarding grant.
Manual refund policy
The starter separates refund assessment from refund execution.
Set an exclusive subscription usage threshold:
PAYMENTS_SUBSCRIPTION_REFUND_MAX_USAGE_PERCENT=5With a value of 5, usage below 5% is eligible for a full-refund recommendation; usage at exactly 5% is not. Credit-package purchases are marked non-refundable by the default policy, except where applicable law requires otherwise.
Run the read-only assessment:
npm run payments:refund:assess -- \
--product subscription \
--granted-credits 500 \
--consumed-credits 24For a package, pass --product credit-package.
The command does not read the database, issue a refund, cancel a subscription, or change credits. Before acting:
- identify the original provider, charge, billing period, and exact credit lot;
- calculate consumption from that lot, not the user's aggregate balance;
- retain the assessment result;
- issue an approved full refund in the original provider's dashboard;
- cancel separately if required;
- manually reconcile the remaining entitlement and record the final provider refund ID.
Provider refund events are not mapped back to credit lots. Automatic reversal, disputes, and chargebacks require manual reconciliation in the current starter.
Verification
npm run preflight
npm run test:unit
npm run test:credit-ledger:integration
npm run test:subscription:integrationThe PostgreSQL integration suites require disposable test databases. Full payment acceptance also requires a provider test payment, public HTTPS webhook delivery, duplicate replay, and database read-back from webhook_inbox, subscriptions, and credit_transactions.