- Open Developers → App notifications in Console (
/workspace/developers?tab=notifications) and select the App and Sandbox environment. - Enter an HTTPS receiver URL, click Generate signing key and store the key in your backend, for example as
INDIANA_NOTIFICATION_KEY. - Turn on Billing events for purchase notifications, then enable and save.
Notifications and webhooks
Receive signed purchase and usage notifications, verify signatures, and answer Funnel external task callbacks.
Set up a receiver
Event types
| type | Sent when |
|---|---|
billing.purchase | A purchase is paid and verified (requires Billing events). |
usage.consume | Usage is recorded. |
usage.confirm | A reservation is confirmed. |
usage.refund | Usage is refunded. |
usage.limit | Usage reaches a set share of its limit (thresholds in manageBillingLimits). |
usage.allowance | Usage reaches a set share of the included allowance (includedThresholds). |
usage.alert | Usage reaches a set quantity (absoluteThresholds). |
Notification types
billing.purchase- Sent when
- A purchase is paid and verified (requires Billing events).
usage.consume- Sent when
- Usage is recorded.
usage.confirm- Sent when
- A reservation is confirmed.
usage.refund- Sent when
- Usage is refunded.
usage.limit- Sent when
- Usage reaches a set share of its limit (
thresholdsinmanageBillingLimits).
usage.allowance- Sent when
- Usage reaches a set share of the included allowance (
includedThresholds).
usage.alert- Sent when
- Usage reaches a set quantity (
absoluteThresholds).
Request format
{ "id": "<DELIVERY_UUID>", "type": "billing.purchase", "projectId": "<APP_UUID>", "environment": "sandbox", "customerId": "<CUSTOMER_UUID>", "occurredAt": "2026-10-22T08:00:00.000Z", "data": {}}| Header | Content |
|---|---|
X-Indiana-Event-Id | The event ID, equal to the body id and unchanged on retries. |
X-Indiana-Signature | t=<Unix seconds>,v1=<HMAC-SHA256 hex> |
X-Indiana-Key-Id | The signing key ID, used during key rotation. |
Headers
X-Indiana-Event-Id- Content
- The event ID, equal to the body
idand unchanged on retries.
X-Indiana-Signature- Content
t=<Unix seconds>,v1=<HMAC-SHA256 hex>
X-Indiana-Key-Id- Content
- The signing key ID, used during key rotation.
- Return 2xx within 8 seconds; store the event first and process it asynchronously.
- The same event can arrive more than once; deduplicate by event ID. Non-2xx responses and timeouts are retried up to 10 times.
- Bodies are at most 64 KiB; amounts and quantities are decimal strings.
Verify the signature
Compute HMAC-SHA256 over <timestamp>.<raw body> with the Base64URL-decoded key, compare it with v1, and reject timestamps older than five minutes. Use the raw request bytes.
import { createHmac, timingSafeEqual } from "node:crypto"; const key = Buffer.from(process.env.INDIANA_NOTIFICATION_KEY!, "base64url");const toleranceSeconds = 300; /** rawBody must be the exact request bytes, read before any JSON parsing. */export function verifyIndianaSignature(rawBody: Buffer, header: string | null) { const parts = Object.fromEntries( (header ?? "").split(",").map((part) => part.split("=", 2)), ); const timestamp = Number(parts.t); if (!Number.isSafeInteger(timestamp) || !parts.v1) return false; if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false; const expected = createHmac("sha256", key) .update(`${timestamp}.`) .update(rawBody) .digest(); const received = Buffer.from(parts.v1, "hex"); return received.length === expected.length && timingSafeEqual(received, expected);} // Your own storage: returns false when this event ID was already recorded.declare function recordOnce(eventId: string, event: unknown): Promise<boolean>; export async function POST(request: Request) { const rawBody = Buffer.from(await request.arrayBuffer()); if (!verifyIndianaSignature(rawBody, request.headers.get("X-Indiana-Signature"))) return new Response("invalid signature", { status: 401 }); const event = JSON.parse(rawBody.toString("utf8")) as { id: string; type: string }; await recordOnce(event.id, event); // persist first, process asynchronously return new Response(null, { status: 200 });}Answer Funnel external tasks
A Funnel external task node sends a funnel.task request to the same receiver with the same signature. Return outcome and data in a 2xx response; data must match the node's declared outputs and stay within 8 KiB.
{ "type": "funnel.task", "eventId": "<RECEIPT_UUID>", "runId": "<RUN_UUID>", "nodeId": "<NODE_ID>", "visit": 1, "projectId": "<APP_UUID>", "environmentId": "<ENVIRONMENT_UUID>", "customerId": "<CUSTOMER_UUID>", "taskKey": "check_eligibility", "input": { "plan": "pro" }}429 and 5xx are retried under the node settings (by default up to 4 attempts); other errors fail. Deduplicate by eventId.