> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.thepennyinc.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.thepennyinc.com/_mcp/server.

# Live Decisioning

Live Decisioning is a **synchronous, blocking** request Penny sends to one of your webhook endpoints in the middle of authorizing a transaction — not the asynchronous, fire-and-forget events described in [Webhooks Overview](/platform/webhooks-overview). Penny waits for your response (or a timeout) before the transaction can proceed, so this is fundamentally different from ordinary event delivery.

> **Info**
>
> Live Decisioning is configured through a Spend Control's `live_decisioning` rule, not through a webhook's `trigger_events` subscriptions. See [Spend Controls](/get-started/platform-concepts/spend-controls) for where this fits among the other rule types and the hierarchy it's evaluated in.

## How it's set up

A `live_decisioning` rule has two required configuration fields:

| Key                        | Type    | Meaning                                                                                                                                                                                                         |
| -------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `notification_endpoint_id` | string  | The active Notification Endpoint Penny calls. It is registered and verified like an ordinary webhook endpoint, but does not require `trigger_events`; decision requests are addressed directly to the endpoint. |
| `fallback_result`          | boolean | Required. Decision used on timeout, connection failure, or invalid response: `true` approves and `false` declines.                                                                                              |

**`A live_decisioning rule's configuration`**

```json title="A live_decisioning rule's configuration"
{
  "notification_endpoint_id": "notification_endpoint_01937b28-f048-77de-8ef4-f0487deef42d",
  "fallback_result": false
}
```

The rule configuration has no timeout field. By default, Penny waits 1 second for your response. The timeout in effect for each request is sent as `request.timeout_seconds`.

## The request Penny sends

Penny sends a JSON `POST` to the configured endpoint. Optional values are shown as `null` where unavailable; actual values vary with the authorization.

**`POST to your webhook endpoint`**

```json title="POST to your webhook endpoint"
{
  "exchange_id": "notification_decision_0193726c-a65e-76c3-8bfb-a65e6c3bfb2f",
  "type": "transaction.authorization",
  "schema_version": "1.0",
  "request_time": "2026-08-25T14:03:11.123456+00:00",
  "request": {
    "identifier": "transaction.authorization",
    "subject": {
      "object_type": "transaction",
      "object_id": "transaction_01937a7d-ad6b-77ca-8046-ad6b7ca046ce"
    },
    "timeout_seconds": 1.0,
    "fallback": "declined",
    "authorization": {
      "type": "debit",
      "transaction_id": "transaction_01937a7d-ad6b-77ca-8046-ad6b7ca046ce",
      "business_id": "business_01937b95-484e-75af-8c68-484e5afc68d3",
      "business_entity_id": "business_entity_01a083e9-ea37-755a-a787-75b0f26fd863",
      "program_id": "program_019377de-7f88-7f02-894f-7f88f0294fcb",
      "account_id": "account_019372e3-b8dc-7de2-80b6-b8dcde20b6c9",
      "card_id": "card_019375de-a2a0-7f31-884a-a2a0f3184abd",
      "cardholder_id": "cardholder_01937e19-4ebb-75bd-8250-4ebb5bd25085",
      "base_amount": { "amount": "108.50", "currency": "USD" },
      "purchase_amount": null,
      "estimated_fee": null,
      "merchant": {
        "merchant_id": "WFM-0481726",
        "code": "5411",
        "category": "Grocery Stores, Supermarkets",
        "name": "Whole Foods Market",
        "city": "Austin",
        "country": "US",
        "subdivision": "US-TX",
        "zip_code": "78701"
      },
      "processing_details": {
        "pan_entry_mode": "contactless",
        "type": "point_of_service",
        "bin_id": "bin_019374af-0cc8-703d-852e-0cc803d52ee3",
        "bin_range_id": "bin_range_019374af-0cc8-703d-852e-0cc803d52ee3",
        "card_product_id": "card_product_019374af-0cc8-703d-852e-0cc803d52ee3",
        "card_class": "consumer"
      },
      "scheme_details": {
        "network": "visa",
        "funding_type": "prepaid",
        "product_type": "consumer"
      }
    }
  }
}
```

`exchange_id` is the decision exchange identifier, and `type` and `request.identifier` are `transaction.authorization`. `request.subject` identifies the transaction. `request.fallback` is `approved` or `declined`, derived from the rule's boolean `fallback_result`; `request.timeout_seconds` is the effective timeout, sent as a JSON number.

`authorization.type` is the authorization direction (`debit`, `credit`, or `adjustment`), not a payment channel. Optional IDs, amounts, and detail fields may be `null`.

| Authorization field                                                | What Penny sends                                                                                                                                                                                                |
| ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `transaction_id`, `account_id`, `card_id`                          | IDs for the transaction being authorized, its funding account, and the card.                                                                                                                                    |
| `business_id`, `business_entity_id`, `program_id`, `cardholder_id` | Business, entity, program, and cardholder IDs when present.                                                                                                                                                     |
| `base_amount`                                                      | Required amount in the card's billing currency.                                                                                                                                                                 |
| `purchase_amount`                                                  | Merchant-currency amount when available; otherwise `null`.                                                                                                                                                      |
| `estimated_fee`                                                    | Estimated fee amount when known; otherwise `null`.                                                                                                                                                              |
| `merchant`                                                         | `merchant_id` (the card network's own merchant identifier, reported at authorization time), `code` (MCC), `category`, `name`, `city`, `country`, `subdivision`, and `zip_code`. Location details can be `null`. |
| `processing_details`                                               | `pan_entry_mode`, processing `type`, `bin_id`, `bin_range_id`, `card_product_id`, and `card_class`.                                                                                                             |
| `scheme_details`                                                   | Network, funding, and product types. Scheme card, program, trace, and transaction identifiers are internal to Penny and not included here.                                                                      |

Every amount uses `{ "amount": "<decimal string>", "currency": "<ISO 4217 code>" }`. For a single-currency transaction, `purchase_amount` may be `null`; use `base_amount` for the amount Penny authorizes.

## The response Penny expects

Respond within the timeout with `2xx` and this body:

**`Your response`**

```json title="Your response"
{
  "outcome": "approved",
  "reason": "Cardholder passed step-up verification"
}
```

* `outcome` — `"approved"` or `"declined"`. Required.
* `reason` — optional free-text explanation, useful for your own audit trail; Penny doesn't act on its content beyond storing it.

Any of the following is treated as a failure, and the rule's `fallback_result` is applied instead:

* No response within `timeout_seconds`.
* A connection error or non-2xx status code.
* A 2xx response whose body is missing, isn't a JSON object, or has an `outcome` value other than `approved`/`declined`.

## Verifying the request

Live Decisioning requests go through the exact same signing mechanism as ordinary event deliveries — if the endpoint has a signing secret configured, the request carries `Penny-Webhook-Timestamp` and `Penny-Webhook-Signature` headers computed the same way, and if it has an `authentication_token`, the request carries the same `Authorization: Bearer` header. See [Verifying webhook signatures](/platform/webhook-hmac) and [Webhook Authorization](/platform/webhook-authorization) for both checks.

## Evaluation order

Only the smallest control surface with a `live_decisioning` rule attached is called — a matching rule at a larger control surface is skipped entirely once a smaller one has run. See [Spend Controls → Multiple applicable controls](/get-started/platform-concepts/spend-controls#multiple-applicable-controls) for the full hierarchy and a worked example.

## The decision exchange record

Every Live Decisioning request Penny sends is recorded as a **decision exchange**: one request/response round trip with your endpoint. Penny keeps the same record whether the exchange resolves normally, times out, or fails, and you can read it through the API:

```http
GET /notifications/webhooks/exchanges?notification_endpoint_id={notification_endpoint_id}
GET /notifications/webhooks/exchanges/{notification_decision_exchange_id}
```

* `GET /notifications/webhooks/exchanges` lists exchanges for one endpoint. `notification_endpoint_id` is required, and results are paginated like any other list endpoint.
* `GET /notifications/webhooks/exchanges/{notification_decision_exchange_id}` retrieves a single exchange. Pass `?version=` to look up a specific past version; it defaults to `0`, which always resolves to the latest.

The exchange response looks like this (`request.authorization` abridged):

**`GET /notifications/webhooks/exchanges/{notification_decision_exchange_id}`**

```jsonc title="GET /notifications/webhooks/exchanges/{notification_decision_exchange_id}"
{
  "notification_decision_exchange_id": "notification_decision_0193726c-a65e-76c3-8bfb-a65e6c3bfb2f",
  "version": 2,
  "version_time": "2026-08-25T14:03:11.654321+00:00",
  "business_id": "business_01937b95-484e-75af-8c68-484e5afc68d3",
  "notification_endpoint_id": "notification_endpoint_01937b28-f048-77de-8ef4-f0487deef42d",
  "request_time": "2026-08-25T14:03:11.123456+00:00",
  "response_time": "2026-08-25T14:03:11.654321+00:00",
  "channel": "webhook",
  "status": "decided",
  "outcome": "approved",
  "fallback_applied": false,
  "request": {
    "identifier": "transaction.authorization",
    "timeout_seconds": 1.0,
    "fallback": "approved",
    "authorization": {
      "transaction_id": "transaction_01937a7d-ad6b-77ca-8046-ad6b7ca046ce"
      // … the same authorization object shown above
    }
  },
  "response": {
    "outcome": "approved",
    "reason": "Cardholder passed step-up verification"
  },
  "metadata": null
}
```

| Field                               | Type                | Meaning                                                                                                                                                                                         |
| ----------------------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `notification_decision_exchange_id` | string              | Unique identifier for this exchange — the same value delivered to you as `exchange_id`.                                                                                                         |
| `version` / `version_time`          | integer / timestamp | Increases each time the exchange's state changes (for example, `pending` → `decided`); use it to confirm you're looking at the latest state.                                                    |
| `business_id`                       | string              | The business the exchange belongs to.                                                                                                                                                           |
| `notification_endpoint_id`          | string              | The Notification Endpoint the decision was requested from; matches the rule's `notification_endpoint_id`.                                                                                       |
| `request_time`                      | timestamp           | When Penny sent the request.                                                                                                                                                                    |
| `response_time`                     | timestamp, optional | When a response, timeout, or failure was recorded.                                                                                                                                              |
| `channel`                           | string              | The delivery channel used — `webhook` for these exchanges.                                                                                                                                      |
| `status`                            | string              | `pending`, `decided`, `timed_out`, or `failed` — see below.                                                                                                                                     |
| `outcome`                           | string, optional    | `approved` or `declined`. Set from your response once `decided`, or from the rule's `fallback_result` on `timed_out` or `failed`. Unset while `pending`.                                        |
| `fallback_applied`                  | boolean             | Whether `outcome` came from the rule's fallback rather than your endpoint's own response.                                                                                                       |
| `request`                           | object              | `identifier`, `timeout_seconds`, `fallback`, and `authorization`. Unlike the webhook body, the exchange response omits `subject`; the transaction ID remains in `authorization.transaction_id`. |
| `response`                          | object, optional    | The `{outcome, reason}` your endpoint returned, once `decided`.                                                                                                                                 |
| `metadata`                          | object, optional    | Reserved for additional context.                                                                                                                                                                |

`status` reflects exactly how the exchange resolved:

| Status      | Meaning                                                                          |
| ----------- | -------------------------------------------------------------------------------- |
| `pending`   | The request has been sent; no response yet.                                      |
| `decided`   | Your endpoint returned a valid `{outcome, reason}` within the timeout.           |
| `timed_out` | No response arrived within `timeout_seconds`.                                    |
| `failed`    | Your endpoint errored, or returned something Penny couldn't parse as a decision. |

Both `timed_out` and `failed` apply the rule's `fallback_result` to `outcome` in exactly the same way — the only difference between them is whether anything came back from your endpoint at all.

The supported Live Decisioning type is `transaction.authorization`; the request shape above applies to that type.

## Related concepts

* [Spend Controls](/get-started/platform-concepts/spend-controls)
* [Webhooks Overview](/platform/webhooks-overview)
* [Subscribing to Webhooks](/platform/subscribing-to-webhooks)
* [Event Catalog](/platform/webhooks-event-catalog)