# Webhooks

> Get Anytime Capital Payments events by webhook: payments, refunds, disputes, invoices and subscriptions, signed with HMAC-SHA256 and retried for 24 hours.

Source: https://anytime-capital.com/docs/webhooks · Markdown: https://anytime-capital.com/docs/webhooks.md · All docs: https://anytime-capital.com/docs/llms-full.txt

Add an endpoint in **Settings → Developers** in your dashboard and we POST a JSON event to it as things happen. Each event wraps the same object the API returns, in `data`, so one parser handles both. `livemode` is `false` for events from [test mode](https://anytime-capital.com/docs/test-mode.md), which only ever go to endpoints added in test mode.

**An event**

```json
{
  "id": "evt_3f9a1c2b7d4e6f8a0b1c2d3e4f5a6b7c",
  "type": "payment.succeeded",
  "created": "2026-10-14T16:02:16.000Z",
  "livemode": true,
  "data": {
    "object": "payment",
    "livemode": true,
    "id": "pay_7Hq2LmX9cR4tVb",
    "status": "paid",
    "amountCents": 12500,
    "amountRefundedCents": 0,
    "netCents": 12088,
    "currency": "usd",
    "description": "Consulting — October",
    "source": {
      "type": "payment_link",
      "id": "pl_4kTz8QwN2xYb6RcD1mPa"
    },
    "paymentLinkId": "pl_4kTz8QwN2xYb6RcD1mPa",
    "customer": {
      "name": "Jordan Lee",
      "email": "jordan@example.com",
      "country": "US"
    },
    "card": {
      "brand": "visa",
      "last4": "4242"
    },
    "paymentMethod": {
      "type": "card",
      "label": "Visa •••• 4242"
    },
    "clientReference": "order_1042",
    "metadata": {
      "campaign": "fall-launch"
    },
    "amountDetails": {
      "subtotalCents": 12500,
      "discountCents": 0,
      "shippingCents": 0,
      "taxCents": 0,
      "customerFeeCents": 0
    },
    "dispute": null,
    "failureMessage": null,
    "createdAt": "2026-10-14T16:02:11.000Z",
    "paidAt": "2026-10-14T16:02:15.000Z"
  }
}
```

## Add an endpoint

1. In **Settings → Developers**, choose **Add endpoint**.
2. Enter an `https://` URL on a public domain name (not an IP address or `localhost`).
3. Choose the events to receive, or every event, including types added later.
4. Copy the endpoint's signing secret (`whsec_…`), shown when you add the endpoint, into a server-side environment variable such as `ANYTIME_WEBHOOK_SECRET`. Owners can **Reveal** it again later.
5. Choose **Send test event** to check your endpoint answers `2xx`. Pick any event type to receive a realistic sample of it.

A business can have up to 10 endpoints, each with its own secret and event selection. Owners and admins manage them. To add and remove endpoints from your own code, REST-hook style, use the [webhook endpoints API](https://anytime-capital.com/docs/webhook-endpoints.md).

## Event types

| Type | `data` is a | When |
| --- | --- | --- |
| `payment.succeeded` | payment | A payment went through: card, bank or stablecoin. Sent once per payment. |
| `payment.failed` | payment | A card payment was declined or blocked. |
| `payment.refunded` | payment | All or part of a payment was refunded. Sent for each refund. |
| `refund.created` | refund | A refund was made on a payment, from the dashboard, the API or an approved request. |
| `refund.succeeded` | refund | A refund reached the customer's card or account. |
| `refund.failed` | refund | A refund failed or was canceled; nothing was returned. |
| `dispute.opened` | payment | A customer disputed a payment with their bank. |
| `dispute.updated` | dispute | A dispute moved on: evidence was submitted or its status changed. |
| `dispute.closed` | payment | A dispute was decided: won, lost or closed. |
| `invoice.created` | invoice | An invoice was created, including each copy of a recurring invoice. |
| `invoice.sent` | invoice | An invoice was emailed to its customer for the first time. |
| `invoice.paid` | invoice | An invoice was paid in full by card. |
| `invoice.overdue` | invoice | An unpaid invoice passed its due date. Sent once per invoice. |
| `invoice.voided` | invoice | An invoice was voided and can no longer be paid. |
| `subscription.created` | subscription | A customer subscribed to one of your plans. |
| `subscription.updated` | subscription | A subscription changed: renewed, set to cancel, paused, resumed or past due. |
| `subscription.paused` | subscription | Payments on a subscription were paused. |
| `subscription.resumed` | subscription | A paused subscription started collecting payments again. |
| `subscription.past_due` | subscription | A renewal payment failed and the subscription is past due. |
| `subscription.canceled` | subscription | A subscription ended: canceled, expired or completed. |
| `payment.review_required` | payment | A card payment was authorized and is waiting for your approval before it is captured. |
| `customer.created` | customer | A customer was added, by hand, from the API or when they first paid. |
| `customer.updated` | customer | A customer's details were changed. |
| `payout.paid` | payout | A payout to your bank account was sent. |
| `payout.failed` | payout | A payout failed, was denied or came back. |
| `webhook.test` | test | Sent by “Send test event”, whatever the endpoint subscribes to. |

**Send test event** can also send any type above with realistic sample data, whatever the endpoint subscribes to. Samples carry `"test": true` at the top level and ids containing `_test_` (such as `pay_test_7Hq2LmX9cR4tVb`), they never appear in the Events API, and they never count towards turning an endpoint off. Never fulfil an order from an event with `test: true`.

Payment events and `dispute.opened` / `dispute.closed` carry the [payment object](https://anytime-capital.com/docs/payments.md#the-payment-object) (with `paymentLinkId`, the link's `metadata` and the customer's email); `refund.*` the [refund object](https://anytime-capital.com/docs/refunds.md#the-refund-object) and `dispute.updated` the [dispute](https://anytime-capital.com/docs/disputes.md#the-dispute-object); invoice events the [invoice object](https://anytime-capital.com/docs/invoices.md#the-invoice-object); subscription events the [subscription object](https://anytime-capital.com/docs/subscriptions.md#the-subscription-object); customer events the [customer object](https://anytime-capital.com/docs/customers.md#the-customer-object); and payout events the [payout object](https://anytime-capital.com/docs/payouts-api.md#the-payout-object).

## Delivery and retries

- Respond with any `2xx` status within 10 seconds to acknowledge an event; do slow work after responding.
- Anything else (another status, a redirect, a timeout) is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 24 hours, then marked failed.
- If several events in a row fail every retry, we turn the endpoint off and email your owners and admins. Fix it, then choose **Test and turn on**: we send a test event and turn the endpoint back on only if it answers `2xx`.
- Delivery is at least once and events can arrive out of order: use `id` (also in the `Anytime-Event-Id` header, the same on every retry and resend) to skip events you've handled, and re-read the object from the API when order matters.

| Header | Value |
| --- | --- |
| `Anytime-Signature` | `t=<unix seconds>,v1=<hex HMAC-SHA256>`, with a second `v1=` during a secret rotation's overlap |
| `Anytime-Event-Id` | The event's `id`: the same on every retry and resend. |
| `Anytime-Event-Type` | The event's `type`. |
| `Anytime-Delivery-Attempt` | Which attempt this is at delivering the event to this endpoint: `1`, then `2`, `3`… on retries and resends. |
| `Content-Type` | `application/json` |

## Delivery log, resends and replays

- Each endpoint's page in **Settings → Developers** shows its health (success rate and latency over 24 hours and 7 days, and its last failure) and its delivery log. Filter by status, event type and time, or search by event id or the id of the object the event is about (`pay_…`, `re_…`, `inv_…`).
- Open a delivery to see every attempt: the headers we sent, the request body, the response status and the start of the response body (truncated, with anything that looks like a key or token removed), the latency and when the next retry goes out.
- **Resend** sends one event again now. From **Events**, any event of the last 30 days can be sent to any of your endpoints, including one that never got it.
- **Resend failed** sends every failed event in a time range again; **Replay since…** sends every event since a time again, including ones the endpoint never got because it was off. Use it after an outage. Each run queues up to 1,000 events (run it again for more), which go out oldest first within minutes. Event ids stay the same, so a handler that skips ids it has seen handles each event once.
- Resends and replays are rate-limited and recorded in your activity log.

## Rotating the signing secret

Owners and admins can replace an endpoint's secret at any time with **Rotate secret**, choosing how long the old one keeps working: 1 hour, 24 hours, 3 days, 7 days, or not at all. During the overlap every event is signed with both secrets, as two `v1=` signatures (the new secret's first), so your endpoint keeps verifying events whichever secret it has. Deploy the new secret before the overlap ends; after that only the new one signs. Rotating again during an overlap drops the oldest secret at once. The Node SDK, the WooCommerce plugin and the Zapier app accept any matching `v1`.

## Verifying signatures

Every event carries an `Anytime-Signature` header such as `t=1760630536,v1=5257a869…`. `v1` is the hex HMAC-SHA256, keyed with your endpoint's signing secret (the whole `whsec_…` string), of the timestamp, a period and the raw request body. Compute it yourself, compare in constant time against every `v1` in the header (there are two during a secret rotation), accept the event if any matches, and reject timestamps more than 5 minutes old so a captured request can't be replayed.

> **Use the raw body:** The signature covers the exact bytes we sent. Verify before parsing, and don't verify a body your framework has already parsed and re-serialised: whitespace and key order would change.

**Node.js (Express)**

```javascript
import crypto from "node:crypto";
import express from "express";

const app = express();
const secret = process.env.ANYTIME_WEBHOOK_SECRET; // whsec_…

// Read the raw body: the signature covers the exact bytes we sent.
app.post("/webhooks/anytime", express.raw({ type: "application/json" }), (req, res) => {
  const header = req.get("Anytime-Signature") ?? "";
  // t=<timestamp>, then one v1= per signing secret (two during a rotation).
  const pairs = header.split(",").map((part) => part.trim().split("="));
  const t = pairs.find(([key]) => key === "t")?.[1] ?? "";
  const signatures = pairs.filter(([key]) => key === "v1").map(([, value]) => value);
  const body = req.body.toString("utf8");

  const expected = Buffer.from(
    crypto.createHmac("sha256", secret).update(`${t}.${body}`).digest("hex"),
  );
  const signed = signatures.some(
    (signature) => signature.length === expected.length && crypto.timingSafeEqual(Buffer.from(signature), expected),
  );
  const fresh = Math.abs(Date.now() / 1000 - Number(t)) <= 300;
  if (!signed || !fresh) return res.status(400).send("Invalid signature");

  const event = JSON.parse(body);
  // The same event can arrive more than once: skip ids you've already handled.
  switch (event.type) {
    case "payment.succeeded":
      // Fulfil the order for event.data.id
      break;
    case "payment.refunded":
      // event.data.amountRefundedCents is the total refunded so far
      break;
  }
  res.sendStatus(200);
});
```

The same check for Next.js, Flask, FastAPI and PHP, and how to send yourself a signed test event, is on [Verify webhooks](https://anytime-capital.com/docs/webhook-signatures.md).

## Events API

Every event we emit for your business is kept for 30 days, whether or not an endpoint took it, and can be read with the `events` permission (read only). Use it to reconcile, or to catch up after your endpoint missed events: list what happened since your last processed event and handle what you haven't seen. Events are recorded once your business has a webhook endpoint or an API key. Each is the same envelope a webhook delivery carries, plus `object` and `pendingWebhooks` (endpoints still retrying it). Endpoints and parameters: [Events API reference](https://anytime-capital.com/docs/events.md).

## Handling events well

- Make your handler idempotent: record each `id` you process (for example in a table with `id` as its primary key, inserted in the same transaction as the work) and skip repeats. Retries, resends and replays all carry the same `id`.
- Don't rely on order: a retry can arrive after a later event. Compare the object's own state (its `status`, timestamps or `amountRefundedCents`) with what you have, or re-read it from the API, before acting.
- Acknowledge fast and work after: return `2xx` once the event is safely stored, then process it in the background.
- Catch up after downtime with the Events API (`GET /events?createdFrom=…`) or **Replay since…**, and let your id check drop anything you'd already handled. `Anytime-Delivery-Attempt` above `1` tells you an event is a retry.
- Skip events with `test: true`: they are samples from **Send test event**.
- Fulfil orders on `payment.succeeded`, never on the customer reaching your success page: a bank account payment can take days, and a customer can close the tab.
- Match a payment to your order with `data.paymentLinkId` and `data.metadata` (from the link), or `data.clientReference` (from `?ref=`); `data.customer.email` is the buyer.
- In Node.js, `constructEvent` in the [Node SDK](https://anytime-capital.com/docs/node-sdk.md#webhooks) verifies the signature and parses the event in one call.
- On `payment.refunded`, `data.amountRefundedCents` is the total refunded so far, not this refund alone.
- On `dispute.opened`, respond from **Disputes** in the dashboard before the deadline.

---

Anytime Capital is a fintech company, not a bank. Payment processing, banking and payout services are provided by Anytime Capital's licensed partners. Rates shown are for US card payments; international cards, currency conversion and disputes may carry additional fees shown in your dashboard before you accept a payment.
