# Webhook Events Reference

Complete list of all webhook events you can subscribe to.

---

## Overview

Webhooks allow your application to receive real-time notifications when events occur in AuxCORE. Instead of polling for changes, AuxCORE sends HTTP POST requests to your configured endpoint.

AuxCORE emits **seventeen** merchant events: eight transaction events, two checkout-session events and seven [dispute events](#dispute-events). The first ten are the default set — `POST /public/register-webhook` subscribes to them when you send no `events`. Dispute events are delivered only when you list them. Subscribing to an unknown name is rejected with `400 UNKNOWN_EVENT_TYPE`. Wildcards (`transaction.*`) and payload filters are not supported.

> **Note:** Subscription, invoice, settlement, fraud, batch and refund-queue events are **not** available as webhooks. Use the REST API to read their state. `invoice.paid` is the one exception outside this list: it is posted to a per-invoice `webhookUrl` (unsigned) — see [Invoices](../invoicing/invoices.md).

---

## Event Structure

All webhook events follow this structure:

```json
{
  "id": "0b8f3d2c-5a1e-4f6b-9c7d-2e4a6b8c0d1f",
  "type": "transaction.approved",
  "created": "2026-01-28T12:34:56.000Z",
  "data": {
    ...event-specific data...
  },
  "metadata": {
    "tenantId": "your-tenant-id",
    "merchantId": "550e8400-e29b-41d4-a716-446655440000"
  }
}
```

| Field | Type | Description |
|-------|------|-------------|
| `id` | string | Unique event ID (also sent as `X-Webhook-Event-ID`) — use it to de-duplicate |
| `type` | string | Event type |
| `created` | string | ISO 8601 time the event was created |
| `data` | object | Event-specific data |
| `metadata.tenantId` | string | Your tenant ID |
| `metadata.merchantId` | string | Merchant the event belongs to |

---

## Event Summary

| Event | Sent when |
|-------|-----------|
| `transaction.approved` | A sale or authorization is approved, or an ACH payment later settles |
| `transaction.pending` | A payment is accepted but not yet settled (for example ACH) |
| `transaction.in_review` | A payment is approved by the processor but held by a Transaction Rule until the merchant reviews it |
| `transaction.declined` | A payment is declined |
| `transaction.voided` | A transaction is voided |
| `transaction.refunded` | A refund is approved |
| `transaction.refund_failed` | A refund is declined by the processor |
| `transaction.returned` | An ACH debit is returned by the bank after it was accepted |
| `session.cancelled` | The shopper cancels a hosted checkout session |
| `session.expired` | A hosted checkout session expires, or the shopper never completes payment |
| `chargeback.received` | A dispute case is opened (API, processor notification or bank-file import) |
| `transaction.chargeback` | A transaction is flagged as charged back from the dashboard |
| `chargeback.open` | A case's status changes to `open` |
| `chargeback.under_review` | A case's status changes to `under_review` |
| `chargeback.won` | A case's status changes to `won` |
| `chargeback.lost` | A case's status changes to `lost` — including when the merchant accepts the dispute |
| `chargeback.expired` | A case's status changes to `expired` (lapsed or prevented) |

Captures do not emit an event; a capture changes the transaction's `type` to `capture`. Delivering a `webhook.test` event is also possible from the test endpoint (see below).

---

## Transaction Events

`transaction.approved`, `transaction.pending`, `transaction.in_review`, `transaction.declined`, `transaction.voided`, `transaction.refunded`, `transaction.refund_failed` and `transaction.returned` all carry the same `data` body. Only `type` and the transaction fields (`status`, `responseCode`, `responseText`, `originalTransactionId`) differ.

```json
{
  "id": "0b8f3d2c-5a1e-4f6b-9c7d-2e4a6b8c0d1f",
  "type": "transaction.approved",
  "created": "2026-01-28T12:34:56.000Z",
  "data": {
    "transactionId": "TXN-1776037573138-56123064",
    "id": "550e8400-e29b-41d4-a716-446655440020",
    "type": "sale",
    "status": "approved",
    "amount": 153.75,
    "baseAmount": 150.00,
    "convenienceFee": 3.75,
    "tipAmount": 0,
    "taxAmount": 0,
    "currency": "USD",
    "authCode": "123456",
    "responseCode": "100",
    "responseText": "Approved",
    "gatewayTransactionId": "8812734455",
    "gatewayName": "NMI",
    "provider": "NMI",
    "paymentMethod": "card",
    "cardLast4": "1111",
    "cardBrand": "visa",
    "orderId": "1042",
    "invoiceNumber": null,
    "billingEmail": "customer@example.com",
    "originalTransactionId": null,
    "requestOrigin": "api",
    "failureReason": null,
    "returnCode": null,
    "payment": { "method": "card" },
    "velocityReviewStatus": null,
    "merchantId": "550e8400-e29b-41d4-a716-446655440000",
    "timestamp": "2026-01-28T12:34:56.000Z"
  },
  "metadata": {
    "tenantId": "your-tenant-id",
    "merchantId": "550e8400-e29b-41d4-a716-446655440000"
  }
}
```

- `transactionId` is the human-readable reference; `id` is the UUID used in API paths.
- `payment.method` is `card`, `ach`, `crypto` or `cash`.
- `failureReason` is a readable reason on `transaction.declined`, `transaction.returned` and `transaction.refund_failed` (for example `Insufficient funds`); `null` otherwise.
- `returnCode` is the ACH return code (`R01`, `R02`, …) on `transaction.returned`; `null` otherwise.
- Hosted checkout payments also carry `sessionId` and `refNo`.
- `status` values: `pending`, `approved`, `declined`, `settled`, `voided`, `refunded`, `error`, `reversed`, `processing`.
- `velocityReviewStatus` is `in_review` while a merchant Transaction Rule holds the payment, then `approved` or `denied`; `null` when no rule held it.

### transaction.approved
A sale or authorization is approved (`status: "approved"`), or a pending ACH payment later settles. For ACH, this event — not `transaction.pending` — means the money arrived.

**When fired:** After the gateway approves the payment.

### transaction.pending
A payment is accepted but not final — typically ACH (`status: "pending"`).

**When fired:** After the payment is accepted for processing.

### transaction.in_review
The processor approved the payment (`status: "approved"`, `velocityReviewStatus: "in_review"`), but one of the merchant's Transaction Rules flagged it, so it is held from settlement until the merchant reviews it. The payment is not complete: keep the order on hold.

**When fired:** After the gateway approves a flagged payment. The merchant's decision follows as `transaction.approved` (Approve) or `transaction.voided` (Deny).

### transaction.declined
`status: "declined"`. `failureReason` is the reason to show or log, `responseCode` an NMI-style decline code and `responseText` the processor text (see [Error Codes](./error-codes.md)). A payment a merchant Transaction Rule denied before it reached the processor has `responseCode: "VELOCITY_DENIED"`.

**When fired:** After the gateway declines the payment.

### transaction.voided
`status: "voided"` — fired for voids of approved sales and authorizations, pending ACH payments and unbatched TSYS captures.

**When fired:** After the void succeeds.

### transaction.refunded
Fired for the refund transaction (`type: "refund"`, `status: "refunded"`). `originalTransactionId` points at the sale.

**When fired:** After the processor approves the refund.

### transaction.refund_failed
Fired for a refund the processor declined (`type: "refund"`, `status: "declined"`); `failureReason` says why. Gateway declines are added to the Refund Decline Queue, but nothing retries them automatically — a user must retry from the queue (see [Refund Queue](../advanced/refund-queue.md)).

**When fired:** After the processor declines the refund.

### transaction.returned
An ACH debit came back from the receiving bank (R01 insufficient funds, R02 account closed, and so on) days after it was accepted. `status` is `reversed`, `returnCode` the R-code (`R01`) and `failureReason` the reason, for example `ACH returned (R01) — Insufficient funds`.

**When fired:** When the bank return is reconciled. For hosted-checkout payments the same body is also sent as a legacy `transaction.declined` event, so a store subscribed to both receives two events.

---

## Session Events

Fired for hosted checkout sessions created with `POST /api/v1/public/checkout-session`.

```json
{
  "id": "5d6e7f80-1a2b-4c3d-8e9f-0a1b2c3d4e5f",
  "type": "session.expired",
  "created": "2026-01-28T12:34:56.000Z",
  "data": {
    "sessionId": "cs_3f9c2b7e0d",
    "refNo": "REF-1042",
    "orderId": "1042",
    "merchantId": "550e8400-e29b-41d4-a716-446655440000",
    "transactionId": "TXN-1776037573138-56123064",
    "status": "error",
    "reason": "checkout_not_completed",
    "timestamp": "2026-01-28T12:34:56.000Z"
  },
  "metadata": {
    "tenantId": "your-tenant-id",
    "merchantId": "550e8400-e29b-41d4-a716-446655440000"
  }
}
```

The example above is the crypto variant (payment started but never completed), which adds `transactionId`, `status: "error"` and `reason`. When the hosted page reports the cancel or expiry itself, `data` carries only `sessionId`, `refNo`, `orderId`, `merchantId` and `timestamp`.

### session.cancelled
The shopper cancelled on the hosted page.

### session.expired
The session expired or the shopper never completed payment.

---

## Dispute Events

Fired by dispute (chargeback) cases. They are **not** part of the default subscription: list each one you want in `events`.

```json
{
  "id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
  "type": "chargeback.received",
  "created": "2026-01-28T12:34:56.000Z",
  "data": {
    "id": "2c4e6a8b-0d1f-4a3c-9e5b-7d9f1b3d5f7a",
    "caseNumber": "CB-2026-00042",
    "merchantId": "550e8400-e29b-41d4-a716-446655440000",
    "originalTransactionId": "550e8400-e29b-41d4-a716-446655440020",
    "auxcoreStatus": "open",
    "networkStage": null,
    "networkStageUpdatedAt": null,
    "outcome": "pending",
    "status": "open",
    "chargebackType": "fraud",
    "reasonCode": "10.4",
    "transactionAmount": 150.75,
    "disputeAmount": 150.75,
    "currency": "USD",
    "receivedDate": "2026-01-28T00:00:00.000Z",
    "dueDate": "2026-02-07T00:00:00.000Z"
  },
  "metadata": {
    "tenantId": "your-tenant-id",
    "merchantId": "550e8400-e29b-41d4-a716-446655440000"
  }
}
```

Every dispute event carries this `data` body:

| Field | Description |
|-------|-------------|
| `id`, `caseNumber` | The dispute case |
| `originalTransactionId` | The disputed transaction |
| `auxcoreStatus` | `open`, `action_required`, `submitted`, `under_review` or `closed` |
| `networkStage` | The card network's stage as reported by the processor, shown verbatim; `null` until reported |
| `outcome` | `pending`, `merchant_won`, `merchant_lost`, `merchant_accepted` or `prevented` |
| `status` | Summary status: `open`, `under_review`, `won`, `lost` or `expired` — the `chargeback.<status>` events are named after it |
| `chargebackType` | `first_chargeback`, `pre_arbitration`, `arbitration`, `retrieval`, `fraud`, `duplicate`, `not_received`, `not_as_described`, `cancelled`, `credit_not_processed` or `other` |
| `reasonCode`, `transactionAmount`, `disputeAmount`, `currency`, `receivedDate`, `dueDate` | Case details; respond before `dueDate` |

- `chargeback.<status>` fires only when the summary `status` changes. An update that changes only `auxcoreStatus`, `networkStage` or `outcome` without moving `status` sends no event — read the case with the [Disputes API](../fraud/disputes.md) when you need every change.
- `transaction.chargeback` is a separate, older name for a dispute raised by flagging the transaction in the dashboard; it carries the same body.

---

## Platform Events

`batch.resubmit` exists in the event registry but is dispatched only to platform (system) endpoints. A merchant endpoint may list it, but never receives it.

---

## Webhook Delivery

### Headers Sent

AuxCORE sends each event to the URL you registered:

```http
POST https://your-app.example.com/webhooks/auxcore
Content-Type: application/json
User-Agent: AuxVault-Webhooks/1.0
X-Webhook-Signature: t=1769603696,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
X-Webhook-Event-Type: transaction.approved
X-Webhook-Event-ID: 0b8f3d2c-5a1e-4f6b-9c7d-2e4a6b8c0d1f
X-Webhook-Delivery-ID: 7c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f
```

`X-Webhook-Signature` is `t=<unix seconds>,v1=<hex HMAC-SHA256>`, where the HMAC is computed with your endpoint secret over `"<t>.<raw request body>"`. Reject signatures older than 5 minutes. See [Webhooks](../getting-started/webhooks.md) for verification code.

### Retry Logic

A delivery fails on any non-2xx response, a network error, or no response within **30 seconds**. Failed deliveries are retried until the endpoint's `maxRetries` attempts (default **3**, including the first) have been made.

| Attempt | Delay (defaults) |
|---------|------------------|
| 1 | Immediate |
| 2 | ~1 minute |
| 3 | ~2 minutes |

The delay before attempt *n + 1* is `retryDelaySeconds × 2^(n − 1)` (default `retryDelaySeconds` is 60). Retries are picked up by a sweep that runs about once a minute. After the last attempt the delivery is marked failed; retry it manually with `POST /api/v1/webhooks/deliveries/:deliveryId/retry`.

### Expected Response

Your endpoint should return a 2xx status code promptly:

```http
HTTP/1.1 200 OK
```

---

## Best Practices

### DO:
- **Verify signatures** - Ensure webhooks are from AuxCORE
- **Respond quickly** - Return 200 and process asynchronously
- **Handle duplicates** - Use the event ID for idempotency
- **Log all events** - For debugging and audit trail
- **Test thoroughly** - Use the Sandbox environment

### DON'T:
- **Don't do heavy processing** - Respond quickly, process later
- **Don't ignore signatures** - Always verify
- **Don't skip error handling** - Handle all scenarios
- **Don't expose an unauthenticated endpoint** - Use HTTPS and verify the signature
- **Don't retry synchronously** - We handle retries

---

## Testing Webhooks

### Test Event Endpoint

Send a `webhook.test` event to an endpoint:

```http
POST /api/v1/webhooks/endpoints/:id/test
```

```bash
curl -X POST https://uat.auxcore.net/api/v1/webhooks/endpoints/ENDPOINT_ID/test \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "X-Tenant-ID: your-tenant-id"
```

The test event has `type: "webhook.test"` and is signed like a real delivery.

---

## Next Steps

- [Set up webhooks →](../getting-started/webhooks.md)
- [Advanced webhook management →](../advanced/webhooks-advanced.md)
- [View all endpoints →](./endpoints.md)

---

**Need help?** Contact support@auxcore.net
