rename auto-topup webhook to billing.auto_topup_succeeded and update docs
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
---
|
||||
title: "Auto Top-Up Succeeded"
|
||||
openapi: "api/openapi.yml webhook billing.auto_topup_succeeded"
|
||||
---
|
||||
|
||||
### Payload Fields
|
||||
|
||||
<ParamField body="customer_id" type="string" required>
|
||||
The ID of the customer whose balance was topped up.
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="feature_id" type="string" required>
|
||||
The feature ID that was automatically topped up.
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="quantity_granted" type="number" required>
|
||||
The normalized amount of balance granted by the top-up.
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="threshold" type="number" required>
|
||||
The configured balance threshold that triggered the top-up.
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="balance_after" type="number" required>
|
||||
The customer's remaining balance for the feature after the top-up.
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="invoice_mode" type="boolean" required>
|
||||
Whether the auto top-up created a `send_invoice` invoice instead of auto-charging the saved payment method.
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="invoice" type="object" required>
|
||||
The invoice created for the auto top-up.
|
||||
<Expandable title="properties">
|
||||
<ParamField body="stripe_id" type="string" required>
|
||||
The Stripe invoice ID. Use this as a stable dedupe key.
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="status" type="string">
|
||||
The status of the invoice. `"paid"` for auto-charged top-ups; `"open"` for `invoice_mode` top-ups where credits were granted but the invoice has not yet been paid.
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="total" type="number" required>
|
||||
The total amount of the invoice in the smallest currency unit (e.g. cents for USD), matching Stripe's `invoice.total`.
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="currency" type="string" required>
|
||||
The ISO currency code for the invoice.
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="hosted_invoice_url" type="string">
|
||||
URL to the hosted invoice page, if available.
|
||||
</ParamField>
|
||||
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
@@ -271,6 +271,12 @@
|
||||
"api-reference/webhooks/balancesLimitReached"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Billing",
|
||||
"pages": [
|
||||
"api-reference/webhooks/billingAutoTopupSucceeded"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Vercel",
|
||||
"pages": [
|
||||
|
||||
@@ -170,4 +170,8 @@ This limits the customer to 5 auto top-ups per month. Supported intervals: `hour
|
||||
|
||||
<Note>
|
||||
Auto top-ups use burst suppression to prevent duplicate purchases when multiple track events happen in quick succession. There's a 30-second cooldown between top-ups for the same feature.
|
||||
</Note>
|
||||
</Note>
|
||||
|
||||
## Notifications
|
||||
|
||||
Subscribe to the [`billing.auto_topup_succeeded`](/api-reference/webhooks/billingAutoTopupSucceeded) webhook to be notified when a top-up grants credits. The payload includes the granted quantity, the new balance, and the underlying invoice — useful for sending receipts, updating internal ledgers, or reconciling balance after a recharge.
|
||||
@@ -97,6 +97,64 @@ For entity-scoped usage, the payload will also include an `entity_id`:
|
||||
}
|
||||
```
|
||||
|
||||
### billing.auto_topup_succeeded
|
||||
|
||||
Fired when an [auto top-up](/documentation/modelling-pricing/auto-top-ups) successfully grants additional prepaid balance. Useful for sending receipts, updating internal ledgers, or reconciling balance after a recharge.
|
||||
|
||||
For auto-charged top-ups, the event fires only after the Stripe invoice is `paid`. For `invoice_mode` top-ups, the event fires once credits are granted and the invoice is finalized — `invoice.status` will typically be `"open"` until the customer pays.
|
||||
|
||||
Use `invoice.stripe_id` as a stable dedupe key. The top-level `id` field (e.g. `evt_auto_topup_...`) is a unique identifier for the event itself.
|
||||
|
||||
**Example payload (auto-charge):**
|
||||
|
||||
```json expandable
|
||||
{
|
||||
"type": "billing.auto_topup_succeeded",
|
||||
"id": "evt_auto_topup_2abc123",
|
||||
"occurred_at": 1761840000000,
|
||||
"data": {
|
||||
"customer_id": "user_123",
|
||||
"feature_id": "credits",
|
||||
"quantity_granted": 1000,
|
||||
"threshold": 500,
|
||||
"balance_after": 1450,
|
||||
"invoice_mode": false,
|
||||
"invoice": {
|
||||
"stripe_id": "in_1A2B3C4D5E6F",
|
||||
"status": "paid",
|
||||
"total": 1000,
|
||||
"currency": "usd",
|
||||
"hosted_invoice_url": "https://invoice.stripe.com/i/..."
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Example payload (invoice mode):**
|
||||
|
||||
```json expandable
|
||||
{
|
||||
"type": "billing.auto_topup_succeeded",
|
||||
"id": "evt_auto_topup_3xyz456",
|
||||
"occurred_at": 1761840000000,
|
||||
"data": {
|
||||
"customer_id": "user_123",
|
||||
"feature_id": "credits",
|
||||
"quantity_granted": 1000,
|
||||
"threshold": 500,
|
||||
"balance_after": 1450,
|
||||
"invoice_mode": true,
|
||||
"invoice": {
|
||||
"stripe_id": "in_2G3H4I5J6K7L",
|
||||
"status": "open",
|
||||
"total": 1000,
|
||||
"currency": "usd",
|
||||
"hosted_invoice_url": "https://invoice.stripe.com/i/..."
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### balances.usage_alert_triggered
|
||||
|
||||
Fired when a customer crosses a configured usage alert threshold. Usage alerts let you monitor when customers approach or exceed specific usage levels for a feature.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
ACTIVE_STATUSES,
|
||||
type BalancesAutoTopupSucceededInvoice,
|
||||
type BillingAutoTopupSucceededInvoice,
|
||||
type BillingResult,
|
||||
fullCustomerToCustomerEntitlements,
|
||||
getApiBalance,
|
||||
@@ -16,7 +16,7 @@ const getInvoicePayload = ({
|
||||
billingResult,
|
||||
}: {
|
||||
billingResult: BillingResult;
|
||||
}): BalancesAutoTopupSucceededInvoice | null => {
|
||||
}): BillingAutoTopupSucceededInvoice | null => {
|
||||
const stripeInvoice = billingResult.stripe.stripeInvoice;
|
||||
if (!stripeInvoice) return null;
|
||||
|
||||
@@ -91,7 +91,7 @@ const sendAutoTopupSucceededWebhookUnsafe = async ({
|
||||
|
||||
await sendSvixEvent({
|
||||
ctx,
|
||||
eventType: WebhookEventType.BalancesAutoTopupSucceeded,
|
||||
eventType: WebhookEventType.BillingAutoTopupSucceeded,
|
||||
payloadFields: {
|
||||
id: generateId("evt_auto_topup"),
|
||||
occurred_at: Date.now(),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { afterAll, beforeAll, expect, test } from "bun:test";
|
||||
import {
|
||||
type ApiCustomerV5,
|
||||
type BalancesAutoTopupSucceeded,
|
||||
type BillingAutoTopupSucceeded,
|
||||
WebhookEventType,
|
||||
} from "@autumn/shared";
|
||||
import { expectBalanceCorrect } from "@tests/integration/utils/expectBalanceCorrect";
|
||||
@@ -21,10 +21,10 @@ import { Decimal } from "decimal.js";
|
||||
import { makeAutoTopupConfig } from "./utils/makeAutoTopupConfig.js";
|
||||
|
||||
type AutoTopupSucceededPayload = {
|
||||
type: WebhookEventType.BalancesAutoTopupSucceeded;
|
||||
type: WebhookEventType.BillingAutoTopupSucceeded;
|
||||
id: string;
|
||||
occurred_at: number;
|
||||
data: BalancesAutoTopupSucceeded;
|
||||
data: BillingAutoTopupSucceeded;
|
||||
};
|
||||
|
||||
let webhook: WebhookTestSetup;
|
||||
@@ -35,7 +35,7 @@ beforeAll(async () => {
|
||||
const appId = getTestSvixAppId({ svixConfig: ctx.org.svix_config });
|
||||
webhook = await setupWebhookTest({
|
||||
appId,
|
||||
filterTypes: [WebhookEventType.BalancesAutoTopupSucceeded],
|
||||
filterTypes: [WebhookEventType.BillingAutoTopupSucceeded],
|
||||
});
|
||||
playToken = webhook.playToken;
|
||||
});
|
||||
@@ -85,7 +85,7 @@ test.concurrent(`${chalk.yellowBright("auto-topup webhook: successful auto top-u
|
||||
const result = await waitForWebhook<AutoTopupSucceededPayload>({
|
||||
token: playToken,
|
||||
predicate: (payload) =>
|
||||
payload.type === WebhookEventType.BalancesAutoTopupSucceeded &&
|
||||
payload.type === WebhookEventType.BillingAutoTopupSucceeded &&
|
||||
payload.data?.customer_id === customerId,
|
||||
timeoutMs: 30_000,
|
||||
});
|
||||
@@ -156,7 +156,7 @@ test.concurrent(`${chalk.yellowBright("auto-topup webhook: invoice mode fires wi
|
||||
const result = await waitForWebhook<AutoTopupSucceededPayload>({
|
||||
token: playToken,
|
||||
predicate: (payload) =>
|
||||
payload.type === WebhookEventType.BalancesAutoTopupSucceeded &&
|
||||
payload.type === WebhookEventType.BillingAutoTopupSucceeded &&
|
||||
payload.data?.customer_id === customerId,
|
||||
timeoutMs: 30_000,
|
||||
});
|
||||
@@ -210,7 +210,7 @@ test.concurrent(`${chalk.yellowBright("auto-topup webhook: no webhook when balan
|
||||
const result = await waitForWebhook<AutoTopupSucceededPayload>({
|
||||
token: playToken,
|
||||
predicate: (payload) =>
|
||||
payload.type === WebhookEventType.BalancesAutoTopupSucceeded &&
|
||||
payload.type === WebhookEventType.BillingAutoTopupSucceeded &&
|
||||
payload.data?.customer_id === customerId,
|
||||
timeoutMs: 10_000,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const BalancesAutoTopupSucceededInvoiceSchema = z.object({
|
||||
export const BillingAutoTopupSucceededInvoiceSchema = z.object({
|
||||
stripe_id: z.string().meta({
|
||||
description: "The Stripe invoice ID.",
|
||||
}),
|
||||
@@ -18,7 +18,7 @@ export const BalancesAutoTopupSucceededInvoiceSchema = z.object({
|
||||
}),
|
||||
});
|
||||
|
||||
export const BALANCES_AUTO_TOPUP_SUCCEEDED_EXAMPLE = {
|
||||
export const BILLING_AUTO_TOPUP_SUCCEEDED_EXAMPLE = {
|
||||
customer_id: "cus_123",
|
||||
feature_id: "messages",
|
||||
quantity_granted: 100,
|
||||
@@ -34,7 +34,7 @@ export const BALANCES_AUTO_TOPUP_SUCCEEDED_EXAMPLE = {
|
||||
},
|
||||
};
|
||||
|
||||
export const BalancesAutoTopupSucceededSchema = z
|
||||
export const BillingAutoTopupSucceededSchema = z
|
||||
.object({
|
||||
customer_id: z.string().meta({
|
||||
description: "The ID of the customer whose balance was topped up.",
|
||||
@@ -57,17 +57,17 @@ export const BalancesAutoTopupSucceededSchema = z
|
||||
description:
|
||||
"Whether the auto top-up created a send_invoice invoice instead of auto-charging.",
|
||||
}),
|
||||
invoice: BalancesAutoTopupSucceededInvoiceSchema.meta({
|
||||
invoice: BillingAutoTopupSucceededInvoiceSchema.meta({
|
||||
description: "The invoice created for the auto top-up.",
|
||||
}),
|
||||
})
|
||||
.meta({
|
||||
examples: [BALANCES_AUTO_TOPUP_SUCCEEDED_EXAMPLE],
|
||||
examples: [BILLING_AUTO_TOPUP_SUCCEEDED_EXAMPLE],
|
||||
});
|
||||
|
||||
export type BalancesAutoTopupSucceeded = z.infer<
|
||||
typeof BalancesAutoTopupSucceededSchema
|
||||
export type BillingAutoTopupSucceeded = z.infer<
|
||||
typeof BillingAutoTopupSucceededSchema
|
||||
>;
|
||||
export type BalancesAutoTopupSucceededInvoice = z.infer<
|
||||
typeof BalancesAutoTopupSucceededInvoiceSchema
|
||||
export type BillingAutoTopupSucceededInvoice = z.infer<
|
||||
typeof BillingAutoTopupSucceededInvoiceSchema
|
||||
>;
|
||||
@@ -1,6 +1,6 @@
|
||||
export * from "./balances/balancesAutoTopupSucceeded.js";
|
||||
export * from "./balances/balancesLimitReached.js";
|
||||
export * from "./balances/balancesUsageAlertTriggered.js";
|
||||
export * from "./billing/billingAutoTopupSucceeded.js";
|
||||
export * from "./vercel/index.js";
|
||||
export * from "./webhookEventType.js";
|
||||
export * from "./webhookRegistry.js";
|
||||
|
||||
@@ -4,7 +4,8 @@ export enum WebhookEventType {
|
||||
|
||||
BalancesUsageAlertTriggered = "balances.usage_alert_triggered",
|
||||
BalancesLimitReached = "balances.limit_reached",
|
||||
BalancesAutoTopupSucceeded = "balances.auto_topup_succeeded",
|
||||
|
||||
BillingAutoTopupSucceeded = "billing.auto_topup_succeeded",
|
||||
|
||||
VercelResourcesDeleted = "vercel.resources.deleted",
|
||||
VercelResourcesProvisioned = "vercel.resources.provisioned",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { z } from "zod/v4";
|
||||
import { BalancesAutoTopupSucceededSchema } from "./balances/balancesAutoTopupSucceeded.js";
|
||||
import { BalancesLimitReachedSchema } from "./balances/balancesLimitReached.js";
|
||||
import { BalancesUsageAlertTriggeredSchema } from "./balances/balancesUsageAlertTriggered.js";
|
||||
import { BillingAutoTopupSucceededSchema } from "./billing/billingAutoTopupSucceeded.js";
|
||||
import { VercelResourceDeletedSchema } from "./vercel/vercelResourceDeleted.js";
|
||||
import { VercelResourceProvisionedSchema } from "./vercel/vercelResourceProvisioned.js";
|
||||
import { VercelResourceRotateSecretsSchema } from "./vercel/vercelResourceRotateSecrets.js";
|
||||
@@ -40,12 +40,14 @@ export const webhookRegistry: WebhookDefinition[] = [
|
||||
description:
|
||||
"Fired when a customer reaches the limit for a feature (included allowance, max purchase, or spend limit).",
|
||||
},
|
||||
|
||||
// ── Billing ───────────────────────────────────────────────────────────
|
||||
{
|
||||
eventType: WebhookEventType.BalancesAutoTopupSucceeded,
|
||||
operationId: "balancesAutoTopupSucceeded",
|
||||
eventType: WebhookEventType.BillingAutoTopupSucceeded,
|
||||
operationId: "billingAutoTopupSucceeded",
|
||||
title: "Auto Top-Up Succeeded",
|
||||
schema: BalancesAutoTopupSucceededSchema,
|
||||
group: "Balances",
|
||||
schema: BillingAutoTopupSucceededSchema,
|
||||
group: "Billing",
|
||||
description:
|
||||
"Fired when an automatic top-up grants additional prepaid balance.",
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user