> ## Documentation Index
> Fetch the complete documentation index at: https://docs.trymaven.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks (Notifications)

> Receive real-time notifications when payment sessions complete

# Webhooks

Maven sends an HTTP POST to your webhook URL when a payment session completes or fails. This is the recommended way to get payment results back into your system.

<Info>
  **Voice and chat widget both use the same webhook.** One webhook URL per project, one handler in your code — it processes both channels. Use the `caller` field to distinguish: it's the customer's phone number for voice, and `null` for chat.
</Info>

## When Webhooks Fire

Webhooks are sent when a session reaches a terminal payment status:

| Status               | When                                                                                                                                                         |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `payment-success`    | Charge was authorized **and captured** immediately (default)                                                                                                 |
| `payment-authorized` | Authorize.net auth-only charge succeeded — funds held but **not** captured. You settle the auth yourself in your gateway dashboard or via the gateway's API. |
| `payment-failed`     | Charge declined or gateway error                                                                                                                             |

Webhooks are **not** sent for `expired`, `cancelled`, or `abandoned` sessions — poll the [GET session endpoint](/api-reference/overview) for those.

## Configuring Your Webhook URL

Set a webhook URL per app in the [Maven Dashboard](https://app.trymaven.com):

1. Navigate to your app
2. Go to the **Settings** tab
3. Enter your webhook URL (must be HTTPS in production)
4. Save

## Verifying Webhook Signatures

Your webhook URL is public, so anyone could POST a forged event to it. Maven signs every webhook with an HMAC so you can confirm it genuinely came from us and wasn't altered in transit. Verification is **optional but strongly recommended** for production.

<Info>
  Signing is **backward compatible** — it only adds a header. If you don't verify it, your existing handler keeps working unchanged. Adopt verification whenever you're ready.
</Info>

### The signing secret

Each app has its own signing secret (format `whsec_…`). Find it in the [Dashboard](https://app.trymaven.com) under **App → Settings → Webhook → Signing secret**, where you can reveal, copy, and **rotate** it.

<Warning>
  Rotating generates a new secret and **invalidates the old one immediately**. Update your server with the new value before (or right after) rotating, or signatures will start failing.
</Warning>

### The `Maven-Signature` header

Every webhook request includes:

```
Maven-Signature: t=1718500000,v1=4f3a9c...e1
```

| Part | Meaning                                                  |
| ---- | -------------------------------------------------------- |
| `t`  | Unix timestamp (seconds) when we signed the request      |
| `v1` | `HMAC_SHA256(secret, "{t}.{raw_body}")` as lowercase hex |

The timestamp is part of the signed content, so it can't be altered without breaking the signature — this is what protects you against replay attacks.

### How to verify

1. Read the **raw request body** — the exact bytes, before any JSON parsing/re-serialization.
2. Parse `t` and `v1` from the `Maven-Signature` header.
3. Compute `HMAC_SHA256(secret, "{t}." + raw_body)`.
4. Constant-time compare it against `v1`.
5. Reject if `t` is older than your tolerance (e.g. 5 minutes) to block replays.

<Tabs>
  <Tab title="Python (FastAPI)">
    ```python theme={"dark"}
    import hmac, hashlib, time, json
    from fastapi import FastAPI, Request, HTTPException

    WEBHOOK_SECRET = "whsec_..."  # from the dashboard
    TOLERANCE_SECONDS = 300

    def verify(raw_body: bytes, signature_header: str) -> bool:
        try:
            parts = dict(p.split("=", 1) for p in signature_header.split(","))
            t, sig = parts["t"], parts["v1"]
        except (ValueError, KeyError):
            return False
        if abs(time.time() - int(t)) > TOLERANCE_SECONDS:
            return False  # too old — possible replay
        signed = f"{t}.".encode() + raw_body
        expected = hmac.new(WEBHOOK_SECRET.encode(), signed, hashlib.sha256).hexdigest()
        return hmac.compare_digest(expected, sig)

    app = FastAPI()

    @app.post("/webhooks/maven")
    async def maven_webhook(request: Request):
        raw = await request.body()  # RAW bytes, not request.json()
        if not verify(raw, request.headers.get("Maven-Signature", "")):
            raise HTTPException(status_code=401, detail="invalid signature")
        payload = json.loads(raw)
        # ... trusted
        return {"status": "ok"}
    ```
  </Tab>

  <Tab title="Node (Express)">
    ```js theme={"dark"}
    const crypto = require("crypto");
    const WEBHOOK_SECRET = process.env.MAVEN_WEBHOOK_SECRET;
    const TOLERANCE_SECONDS = 300;

    function verify(rawBody, signatureHeader) {
      const parts = Object.fromEntries(
        (signatureHeader || "").split(",").map((p) => p.split("=")),
      );
      const { t, v1 } = parts;
      if (!t || !v1) return false;
      if (Math.abs(Date.now() / 1000 - Number(t)) > TOLERANCE_SECONDS) return false;
      const expected = crypto
        .createHmac("sha256", WEBHOOK_SECRET)
        .update(`${t}.`)
        .update(rawBody) // Buffer of the raw body
        .digest("hex");
      return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
    }

    // Capture the raw body so the bytes match what we signed:
    app.post(
      "/webhooks/maven",
      express.raw({ type: "application/json" }),
      (req, res) => {
        if (!verify(req.body, req.get("Maven-Signature"))) {
          return res.status(401).send("invalid signature");
        }
        const payload = JSON.parse(req.body.toString());
        res.sendStatus(200);
      },
    );
    ```
  </Tab>
</Tabs>

<Warning>
  **Sign the raw bytes, not the parsed JSON.** `{"a":1,"b":2}` and `{"b":2,"a":1}` are equal objects but different bytes, so re-serializing before hashing produces a mismatching signature. Always HMAC the request body exactly as received, and use a constant-time comparison (`hmac.compare_digest` / `crypto.timingSafeEqual`).
</Warning>

<Tip>
  Use the **Test** button next to your webhook URL in the dashboard — it sends a signed sample payload so you can validate your verification code end-to-end.
</Tip>

## Payload Format

All webhook payloads share the same top-level fields. The `processor` object varies by gateway and mode — see [Processor Fields by Gateway](#processor-fields-by-gateway) for the full specs.

### Voice vs Chat

The payload is **almost identical for voice and chat** — only one field differs:

* **`caller`** is the customer's phone number for voice sessions, and `null` for chat sessions (no phone involved)

Everything else — `status`, `processor`, `card_brand`, `card_last4`, error codes — is the same. A single webhook handler works for both.

<Tabs>
  <Tab title="Chat (Widget)">
    ```json theme={"dark"}
    {
      "session_id": "a1b2c3d4-...",
      "status": "payment-success",
      "project": "my-store",
      "environment": "live",
      "amount": 49.99,
      "currency": "USD",
      "mode": "charge",
      "gateway": "stripe",
      "caller": null,
      "card_brand": "visa",
      "card_last4": "4242",
      "processor": {
        "payment_intent_id": "pi_xxx",
        "charge_id": "ch_xxx",
        "card_brand": "visa",
        "card_last4": "4242"
      }
    }
    ```
  </Tab>

  <Tab title="Voice (Phone)">
    ```json theme={"dark"}
    {
      "session_id": "a1b2c3d4-...",
      "status": "payment-success",
      "project": "my-store",
      "environment": "live",
      "amount": 49.99,
      "currency": "USD",
      "mode": "charge",
      "gateway": "stripe",
      "caller": "+14155551234",
      "card_brand": "visa",
      "card_last4": "4242",
      "processor": {
        "payment_intent_id": "pi_xxx",
        "charge_id": "ch_xxx",
        "card_brand": "visa",
        "card_last4": "4242"
      }
    }
    ```
  </Tab>
</Tabs>

## Full examples by gateway

<Tabs>
  <Tab title="Stripe (Charge)">
    ```json theme={"dark"}
    {
      "session_id": "a1b2c3d4-...",
      "status": "payment-success",
      "project": "my-store",
      "environment": "live",
      "amount": 49.99,
      "currency": "USD",
      "mode": "charge",
      "gateway": "stripe",
      "caller": "+14155551234",
      "card_brand": "visa",
      "card_last4": "4242",
      "processor": {
        "payment_intent_id": "pi_xxx",
        "charge_id": "ch_xxx",
        "receipt_url": "https://pay.stripe.com/receipts/...",
        "payment_method_id": "pm_xxx",
        "card_brand": "visa",
        "card_last4": "4242"
      }
    }
    ```
  </Tab>

  <Tab title="Stripe (Tokenize)">
    ```json theme={"dark"}
    {
      "session_id": "a1b2c3d4-...",
      "status": "payment-success",
      "project": "my-store",
      "environment": "live",
      "amount": 49.99,
      "currency": "USD",
      "mode": "tokenize",
      "gateway": "stripe",
      "caller": "+14155551234",
      "card_brand": "visa",
      "card_last4": "4242",
      "processor": {
        "payment_method_id": "pm_xxx",
        "cloned_payment_method_id": "pm_yyy",
        "cloned_customer_id": "cus_xxx",
        "card_brand": "visa",
        "card_last4": "4242"
      }
    }
    ```
  </Tab>

  <Tab title="Authorize.net (Charge)">
    ```json theme={"dark"}
    {
      "session_id": "a1b2c3d4-...",
      "status": "payment-success",
      "project": "my-store",
      "environment": "live",
      "amount": 49.99,
      "currency": "USD",
      "mode": "charge",
      "gateway": "authorizenet",
      "caller": "+14155551234",
      "card_brand": "visa",
      "card_last4": "4242",
      "processor": {
        "transaction_id": "80053776892",
        "auth_code": "D2U7TY",
        "response_code": "1",
        "avs_result_code": "Y",
        "cvv_result_code": "M",
        "cavv_result_code": "2",
        "network_trans_id": "V1NZI0NLBXIKFMK2RAC1YCV",
        "auth_only": false,
        "card_brand": "visa",
        "card_last4": "4242"
      }
    }
    ```
  </Tab>

  <Tab title="Authorize.net (Auth-Only)">
    ```json theme={"dark"}
    {
      "session_id": "a1b2c3d4-...",
      "status": "payment-authorized",
      "project": "my-store",
      "environment": "live",
      "amount": 49.99,
      "currency": "USD",
      "mode": "charge",
      "gateway": "authorizenet",
      "caller": "+14155551234",
      "card_brand": "visa",
      "card_last4": "4242",
      "processor": {
        "transaction_id": "80053776894",
        "auth_code": "P60ZS3",
        "response_code": "1",
        "avs_result_code": "Y",
        "cvv_result_code": "M",
        "cavv_result_code": "2",
        "network_trans_id": "DTUM1U4E6VT99F6A5KZQ0SS",
        "auth_only": true,
        "card_brand": "visa",
        "card_last4": "4242"
      }
    }
    ```

    Funds are held but not captured. Use `processor.transaction_id` to settle the charge later in your Authorize.net dashboard or via your own `priorAuthCaptureTransaction` API call. See [Authorize.net Setup → Capture Mode](/integrations/authorizenet-setup#capture-mode).
  </Tab>

  <Tab title="Authorize.net (Tokenize)">
    ```json theme={"dark"}
    {
      "session_id": "a1b2c3d4-...",
      "status": "payment-success",
      "project": "my-store",
      "environment": "live",
      "amount": 49.99,
      "currency": "USD",
      "mode": "tokenize",
      "gateway": "authorizenet",
      "caller": "+14155551234",
      "card_brand": "visa",
      "card_last4": "4242",
      "processor": {
        "customer_profile_id": "123456789",
        "payment_profile_id": "987654321",
        "card_brand": "visa",
        "card_last4": "4242"
      }
    }
    ```
  </Tab>

  <Tab title="Braintree (Charge)">
    ```json theme={"dark"}
    {
      "session_id": "a1b2c3d4-...",
      "status": "payment-success",
      "project": "my-store",
      "environment": "live",
      "amount": 49.99,
      "currency": "USD",
      "mode": "charge",
      "gateway": "braintree",
      "caller": "+14155551234",
      "card_brand": "visa",
      "card_last4": "4242",
      "processor": {
        "transaction_id": "abc123",
        "braintree_status": "submitted_for_settlement",
        "customer_id": "cust_xxx",
        "payment_method_token": "token_xxx",
        "card_brand": "visa",
        "card_last4": "4242"
      }
    }
    ```
  </Tab>

  <Tab title="Braintree (Tokenize)">
    ```json theme={"dark"}
    {
      "session_id": "a1b2c3d4-...",
      "status": "payment-success",
      "project": "my-store",
      "environment": "live",
      "amount": 49.99,
      "currency": "USD",
      "mode": "tokenize",
      "gateway": "braintree",
      "caller": "+14155551234",
      "card_brand": "visa",
      "card_last4": "4242",
      "processor": {
        "customer_id": "cust_xxx",
        "payment_method_token": "token_xxx",
        "card_brand": "visa",
        "card_last4": "4242"
      }
    }
    ```
  </Tab>

  <Tab title="Shift4 (Charge)">
    ```json theme={"dark"}
    {
      "session_id": "a1b2c3d4-...",
      "status": "payment-success",
      "project": "my-store",
      "environment": "live",
      "amount": 49.99,
      "currency": "USD",
      "mode": "charge",
      "gateway": "shift4",
      "caller": "+14155551234",
      "card_brand": "visa",
      "card_last4": "4242",
      "processor": {
        "charge_id": "char_xxx",
        "customer_id": "cust_xxx",
        "card_id": "card_xxx",
        "card_brand": "visa",
        "card_last4": "4242"
      }
    }
    ```
  </Tab>

  <Tab title="Shift4 (Tokenize)">
    ```json theme={"dark"}
    {
      "session_id": "a1b2c3d4-...",
      "status": "payment-success",
      "project": "my-store",
      "environment": "live",
      "amount": 49.99,
      "currency": "USD",
      "mode": "tokenize",
      "gateway": "shift4",
      "caller": "+14155551234",
      "card_brand": "visa",
      "card_last4": "4242",
      "processor": {
        "customer_id": "cust_xxx",
        "card_id": "card_xxx",
        "card_brand": "visa",
        "card_last4": "4242"
      }
    }
    ```
  </Tab>

  <Tab title="Fiserv (Charge)">
    ```json theme={"dark"}
    {
      "session_id": "a1b2c3d4-...",
      "status": "payment-success",
      "project": "my-store",
      "environment": "live",
      "amount": 49.99,
      "currency": "USD",
      "mode": "charge",
      "gateway": "fiserv",
      "caller": "+14155551234",
      "card_brand": "VISA",
      "card_last4": "4977",
      "processor": {
        "fiserv_transaction_id": "84653901038",
        "fiserv_order_id": "R-ddf385f2-...",
        "fiserv_payment_token": "A60759FC-B2E7-40E2-BC78-3C230C5AB7CB",
        "fiserv_state": "CAPTURED",
        "fiserv_status": "APPROVED",
        "approval_code": "279391",
        "response_code": "00",
        "response_message": "Function performed error-free",
        "card_brand": "VISA",
        "card_last4": "4977"
      }
    }
    ```
  </Tab>

  <Tab title="Fiserv (Tokenize)">
    ```json theme={"dark"}
    {
      "session_id": "a1b2c3d4-...",
      "status": "payment-success",
      "project": "my-store",
      "environment": "live",
      "amount": 49.99,
      "currency": "USD",
      "mode": "tokenize",
      "gateway": "fiserv",
      "caller": "+14155551234",
      "card_brand": "VISA",
      "card_last4": "4977",
      "processor": {
        "fiserv_payment_token": "A60759FC-B2E7-40E2-BC78-3C230C5AB7CB",
        "ipg_transaction_id": "84653901037",
        "card_brand": "VISA",
        "card_last4": "4977"
      }
    }
    ```
  </Tab>
</Tabs>

### Field Reference

| Field         | Type           | Description                                                            |
| ------------- | -------------- | ---------------------------------------------------------------------- |
| `session_id`  | string         | Session UUID                                                           |
| `status`      | string         | `"payment-success"`, `"payment-authorized"`, or `"payment-failed"`     |
| `project`     | string         | App slug                                                               |
| `environment` | string         | `"test"` or `"live"`                                                   |
| `amount`      | number         | Amount in **dollars** (e.g., `49.99`)                                  |
| `currency`    | string         | Currency code (e.g., `"USD"`)                                          |
| `mode`        | string         | `"charge"` or `"tokenize"`                                             |
| `gateway`     | string         | `"stripe"`, `"authorizenet"`, `"braintree"`, `"shift4"`, or `"fiserv"` |
| `caller`      | string \| null | Caller phone number in E.164 format (voice only; `null` for chat)      |
| `card_brand`  | string \| null | Card brand (visa, mastercard, amex, etc.)                              |
| `card_last4`  | string \| null | Last 4 digits of the card                                              |
| `processor`   | object \| null | Gateway-specific response fields (see below)                           |
| `error`       | object \| null | Present only when `status` is `"payment-failed"`                       |

### Error Object (failures only)

```json theme={"dark"}
{
  "error": {
    "code": "card_declined",
    "message": "Your card was declined."
  }
}
```

### Processor Fields by Gateway

The `processor` object contains different fields depending on the gateway and mode (`charge` vs `tokenize`).

<Tabs>
  <Tab title="Stripe">
    **Charge mode:**

    ```json theme={"dark"}
    {
      "payment_intent_id": "pi_xxx",
      "charge_id": "ch_xxx",
      "receipt_url": "https://pay.stripe.com/receipts/...",
      "payment_method_id": "pm_xxx",
      "card_brand": "visa",
      "card_last4": "4242",
      "postal_code": "90210",
      "exp_month": 12,
      "exp_year": 2027
    }
    ```

    | Field               | Description                            |
    | ------------------- | -------------------------------------- |
    | `payment_intent_id` | Stripe PaymentIntent ID                |
    | `charge_id`         | Stripe Charge ID                       |
    | `receipt_url`       | Stripe-hosted receipt URL              |
    | `payment_method_id` | Stripe PaymentMethod ID                |
    | `card_brand`        | Card brand (visa, mastercard, etc.)    |
    | `card_last4`        | Last 4 digits of the card              |
    | `postal_code`       | Billing ZIP/postal code (if collected) |
    | `exp_month`         | Card expiration month (if collected)   |
    | `exp_year`          | Card expiration year (if collected)    |

    **Tokenize mode:**

    ```json theme={"dark"}
    {
      "payment_method_id": "pm_xxx",
      "cloned_payment_method_id": "pm_yyy",
      "cloned_customer_id": "cus_xxx",
      "card_brand": "visa",
      "card_last4": "4242",
      "postal_code": "90210",
      "exp_month": 12,
      "exp_year": 2027
    }
    ```

    | Field                      | Description                                                                         |
    | -------------------------- | ----------------------------------------------------------------------------------- |
    | `payment_method_id`        | Original Stripe PaymentMethod ID (on Maven's platform account)                      |
    | `cloned_payment_method_id` | PaymentMethod cloned to your Stripe account (absent if same as `payment_method_id`) |
    | `cloned_customer_id`       | Customer created on your Stripe account                                             |
    | `card_brand`               | Card brand                                                                          |
    | `card_last4`               | Last 4 digits                                                                       |
    | `postal_code`              | Billing ZIP/postal code (if collected)                                              |
    | `exp_month`                | Card expiration month (if collected)                                                |
    | `exp_year`                 | Card expiration year (if collected)                                                 |
  </Tab>

  <Tab title="Authorize.net">
    **Charge mode:**

    ```json theme={"dark"}
    {
      "transaction_id": "80053776892",
      "auth_code": "D2U7TY",
      "response_code": "1",
      "avs_result_code": "Y",
      "cvv_result_code": "M",
      "cavv_result_code": "2",
      "network_trans_id": "V1NZI0NLBXIKFMK2RAC1YCV",
      "auth_only": false,
      "card_brand": "visa",
      "card_last4": "4242",
      "postal_code": "90210",
      "exp_month": 12,
      "exp_year": 2027
    }
    ```

    | Field              | Description                                                                                                                                                  |
    | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
    | `transaction_id`   | Authorize.net transaction ID. In Authorize Only mode, use this as the `refTransId` for `priorAuthCaptureTransaction`.                                        |
    | `auth_code`        | Authorization code from the issuing bank                                                                                                                     |
    | `response_code`    | Authorize.net response code (`1`=Approved, `2`=Declined, `3`=Error, `4`=Held for review)                                                                     |
    | `avs_result_code`  | AVS (Address Verification System) match result. `Y` = address+zip match.                                                                                     |
    | `cvv_result_code`  | CVV match result. `M` = match.                                                                                                                               |
    | `cavv_result_code` | CAVV (3D Secure) result. `2` = passed.                                                                                                                       |
    | `network_trans_id` | Card network transaction ID (Visa/Mastercard) — needed for card-on-file flows and certain refunds                                                            |
    | `auth_only`        | `true` when the project is set to **Authorize Only** capture mode (auth without capture). See [Capture Mode](/integrations/authorizenet-setup#capture-mode). |
    | `card_brand`       | Card brand                                                                                                                                                   |
    | `card_last4`       | Last 4 digits                                                                                                                                                |
    | `postal_code`      | Billing ZIP/postal code (if collected)                                                                                                                       |
    | `exp_month`        | Card expiration month (if collected)                                                                                                                         |
    | `exp_year`         | Card expiration year (if collected)                                                                                                                          |

    **Tokenize mode:**

    ```json theme={"dark"}
    {
      "customer_profile_id": "123456789",
      "payment_profile_id": "987654321",
      "card_brand": "visa",
      "card_last4": "4242",
      "postal_code": "90210",
      "exp_month": 12,
      "exp_year": 2027
    }
    ```

    | Field                 | Description                            |
    | --------------------- | -------------------------------------- |
    | `customer_profile_id` | CIM Customer Profile ID                |
    | `payment_profile_id`  | CIM Payment Profile ID                 |
    | `card_brand`          | Card brand                             |
    | `card_last4`          | Last 4 digits                          |
    | `postal_code`         | Billing ZIP/postal code (if collected) |
    | `exp_month`           | Card expiration month (if collected)   |
    | `exp_year`            | Card expiration year (if collected)    |
  </Tab>

  <Tab title="Braintree">
    **Charge mode:**

    ```json theme={"dark"}
    {
      "transaction_id": "abc123",
      "braintree_status": "submitted_for_settlement",
      "customer_id": "cust_xxx",
      "payment_method_token": "token_xxx",
      "card_brand": "visa",
      "card_last4": "4242",
      "postal_code": "90210",
      "exp_month": 12,
      "exp_year": 2027
    }
    ```

    | Field                  | Description                                           |
    | ---------------------- | ----------------------------------------------------- |
    | `transaction_id`       | Braintree transaction ID                              |
    | `braintree_status`     | Transaction status (e.g., `submitted_for_settlement`) |
    | `customer_id`          | Braintree customer ID                                 |
    | `payment_method_token` | Braintree payment method token                        |
    | `card_brand`           | Card brand                                            |
    | `card_last4`           | Last 4 digits                                         |
    | `postal_code`          | Billing ZIP/postal code (if collected)                |
    | `exp_month`            | Card expiration month (if collected)                  |
    | `exp_year`             | Card expiration year (if collected)                   |

    **Tokenize mode:**

    ```json theme={"dark"}
    {
      "customer_id": "cust_xxx",
      "payment_method_token": "token_xxx",
      "card_brand": "visa",
      "card_last4": "4242",
      "postal_code": "90210",
      "exp_month": 12,
      "exp_year": 2027
    }
    ```

    | Field                  | Description                            |
    | ---------------------- | -------------------------------------- |
    | `customer_id`          | Braintree customer ID                  |
    | `payment_method_token` | Braintree payment method token         |
    | `card_brand`           | Card brand                             |
    | `card_last4`           | Last 4 digits                          |
    | `postal_code`          | Billing ZIP/postal code (if collected) |
    | `exp_month`            | Card expiration month (if collected)   |
    | `exp_year`             | Card expiration year (if collected)    |
  </Tab>

  <Tab title="Shift4">
    **Charge mode:**

    ```json theme={"dark"}
    {
      "charge_id": "char_xxx",
      "customer_id": "cust_xxx",
      "card_id": "card_xxx",
      "card_brand": "visa",
      "card_last4": "4242",
      "postal_code": "90210",
      "exp_month": 12,
      "exp_year": 2027
    }
    ```

    | Field         | Description                            |
    | ------------- | -------------------------------------- |
    | `charge_id`   | Shift4 charge ID                       |
    | `customer_id` | Shift4 customer ID                     |
    | `card_id`     | Shift4 card ID                         |
    | `card_brand`  | Card brand                             |
    | `card_last4`  | Last 4 digits                          |
    | `postal_code` | Billing ZIP/postal code (if collected) |
    | `exp_month`   | Card expiration month (if collected)   |
    | `exp_year`    | Card expiration year (if collected)    |

    **Tokenize mode:**

    ```json theme={"dark"}
    {
      "customer_id": "cust_xxx",
      "card_id": "card_xxx",
      "card_brand": "visa",
      "card_last4": "4242",
      "postal_code": "90210",
      "exp_month": 12,
      "exp_year": 2027
    }
    ```

    | Field         | Description                            |
    | ------------- | -------------------------------------- |
    | `customer_id` | Shift4 customer ID                     |
    | `card_id`     | Shift4 card ID                         |
    | `card_brand`  | Card brand                             |
    | `card_last4`  | Last 4 digits                          |
    | `postal_code` | Billing ZIP/postal code (if collected) |
    | `exp_month`   | Card expiration month (if collected)   |
    | `exp_year`    | Card expiration year (if collected)    |
  </Tab>

  <Tab title="Fiserv">
    **Charge mode:**

    ```json theme={"dark"}
    {
      "fiserv_transaction_id": "84653901038",
      "fiserv_order_id": "R-ddf385f2-...",
      "fiserv_payment_token": "A60759FC-B2E7-40E2-BC78-3C230C5AB7CB",
      "fiserv_state": "CAPTURED",
      "fiserv_status": "APPROVED",
      "approval_code": "279391",
      "response_code": "00",
      "response_message": "Function performed error-free",
      "card_brand": "VISA",
      "card_last4": "4977",
      "postal_code": "90210"
    }
    ```

    | Field                   | Description                                                             |
    | ----------------------- | ----------------------------------------------------------------------- |
    | `fiserv_transaction_id` | Fiserv `ipgTransactionId`                                               |
    | `fiserv_order_id`       | Fiserv `orderId`                                                        |
    | `fiserv_payment_token`  | Reusable Fiserv payment token (also returned on each charge for re-use) |
    | `fiserv_state`          | `"CAPTURED"`                                                            |
    | `fiserv_status`         | `"APPROVED"` on success                                                 |
    | `approval_code`         | Fiserv authorization code                                               |
    | `response_code`         | Processor response code (`"00"` = success)                              |
    | `response_message`      | Processor response message                                              |
    | `card_brand`            | Card brand (e.g. `VISA`)                                                |
    | `card_last4`            | Last 4 digits                                                           |
    | `postal_code`           | Billing ZIP/postal code (if collected)                                  |

    **Tokenize mode:**

    ```json theme={"dark"}
    {
      "fiserv_payment_token": "A60759FC-B2E7-40E2-BC78-3C230C5AB7CB",
      "ipg_transaction_id": "84653901037",
      "card_brand": "VISA",
      "card_last4": "4977",
      "postal_code": "90210"
    }
    ```

    | Field                  | Description                                             |
    | ---------------------- | ------------------------------------------------------- |
    | `fiserv_payment_token` | Reusable Fiserv payment token (UUID)                    |
    | `ipg_transaction_id`   | Fiserv `ipgTransactionId` from the tokenization request |
    | `card_brand`           | Card brand                                              |
    | `card_last4`           | Last 4 digits                                           |
    | `postal_code`          | Billing ZIP/postal code (if collected)                  |
  </Tab>
</Tabs>

## Handling Webhooks

Your webhook endpoint should:

1. Return a `200` status code quickly (within 5 seconds)
2. Process the payload asynchronously if needed
3. Be idempotent — use `session_id` to deduplicate

```python theme={"dark"}
from fastapi import FastAPI, Request

app = FastAPI()

@app.post("/webhooks/maven")
async def maven_webhook(request: Request):
    payload = await request.json()
    session_id = payload["session_id"]
    status = payload["status"]

    if status == "payment-success":
        card_last4 = payload.get("card_last4")
        caller = payload.get("caller")  # phone for voice, None for chat
        processor = payload.get("processor", {})
        # Update your order, send receipt, etc.
        await handle_payment_success(session_id, processor)
    elif status == "payment-failed":
        error = payload.get("error", {})
        await handle_payment_failure(session_id, error)

    return {"status": "ok"}
```

## Best Practices

<AccordionGroup>
  <Accordion title="Always return 200 quickly">
    Return a `200` response immediately and process the webhook asynchronously. Maven uses a 5-second delivery timeout; if your endpoint is slower, the request is treated as failed and retried.
  </Accordion>

  <Accordion title="Use idempotent handlers">
    Use the `session_id` to deduplicate — check if you've already processed this session before acting on it.
  </Accordion>

  <Accordion title="Verify the signature">
    Confirm the event came from Maven by verifying the `Maven-Signature` header — see [Verifying Webhook Signatures](#verifying-webhook-signatures). As an additional check, you can also confirm the `session_id` belongs to your organization via the API:

    ```
    GET /v1/sessions/{session_id}
    ```
  </Accordion>

  <Accordion title="Handle failures gracefully">
    If your endpoint returns a 5xx or times out, Maven retries up to 3 times with exponential backoff (1s, 2s, 4s). A 4xx response is treated as a rejection and is **not** retried. If all attempts fail the webhook is dropped, so for critical flows also poll the session status as a fallback.
  </Accordion>
</AccordionGroup>
