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

# Customer Data Enrichment: Feed Your Own Data Into Recovery

> Expose an HTTP endpoint that Airdun calls at recovery case creation to pull your own customer data into the recovery strategy, messages, and escalation rules.

Enrichment lets Airdun read customer data that only your systems have — plan name, seat count, lifetime value, feature usage — and use it when building a recovery plan.

You expose an HTTP endpoint. Airdun calls it, maps the JSON response onto named fields you define, and stores the result on the recovery case.

<Note>
  **Airdun is the client; you are the server.** There is no ingest API and no way to push data to Airdun. Enrichment is a read, performed by Airdun against an endpoint you host.
</Note>

## When Airdun calls your endpoint

Your endpoint is called **once per recovery case, at case creation** — not once per message, and not once per retry.

The response is projected into a **snapshot** that is written onto the case. Every downstream step reads that frozen snapshot; your endpoint is never called again for that case. Data that changes after a case opens will not be reflected in that case.

A new failed payment opens a new case, which triggers a new call.

## The contract

|                 |                        |
| --------------- | ---------------------- |
| Direction       | Airdun → your endpoint |
| Method          | `GET`                  |
| Request body    | None                   |
| Expected status | Any `2xx`              |
| Expected body   | Any JSON value         |

### URL

Your URL must contain the `{customer_id}` placeholder:

```text theme={null}
https://api.your-app.com/airdun/customers/{customer_id}
```

* The placeholder is replaced with the **Stripe customer ID** (`cus_…`), URL-encoded.
* Only the **first** occurrence of `{customer_id}` is substituted.
* **HTTPS is required.** Plain HTTP is accepted only for `localhost` and `127.0.0.1`.
* Maximum length: 2048 characters.

### Authentication

Three modes are available. Airdun stores your secret encrypted at rest (AES-256-GCM).

<AccordionGroup>
  <Accordion title="NONE">
    No authentication header is sent. Only appropriate if your endpoint is protected some other way — the Stripe customer ID is not a secret.
  </Accordion>

  <Accordion title="BEARER">
    Airdun sends:

    ```http theme={null}
    Authorization: Bearer <your-secret>
    ```
  </Accordion>

  <Accordion title="API_KEY">
    Airdun sends your secret as the raw value of a header you name — no `Bearer` prefix, no scheme:

    ```http theme={null}
    X-Api-Key: <your-secret>
    ```

    The header name is required when this mode is selected. Maximum 128 characters.
  </Accordion>
</AccordionGroup>

### Custom headers

You can add up to 20 additional headers (name ≤ 128 chars, value ≤ 1024 chars).

<Warning>
  Custom headers are applied **after** the authentication header. A custom header whose name collides with your auth header — `Authorization`, or whatever you named your API key header — will silently overwrite it, and your endpoint will reject the call.
</Warning>

### Timeout and failure

* **Timeout** is configurable from **500 ms to 30 000 ms**. Default: **3000 ms**.
* **There are no retries.** A single request is made per case. If it fails, it fails.

Enrichment never blocks recovery. If your endpoint times out, returns a non-2xx status, or returns a body that is not valid JSON, Airdun records the error on the case and **the recovery case is created anyway** with an empty snapshot.

When the snapshot is empty:

* The strategy and message-generation models receive no enrichment context.
* Escalation rules that reference an `enrichment.*` field are **skipped**. Rules that do not reference enrichment still evaluate normally.

<Note>
  A response that is valid JSON but does not match your field mapping is **not** an error. Unmatched paths simply resolve to `null`, and the case proceeds silently. Check the call log in your dashboard if fields arrive empty.
</Note>

## Mapping the response to fields

Airdun does not care about your response shape. You define up to **100 fields**, each of which extracts one value from the JSON.

| Property   | Rules                                                                                                                                       |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`     | 1–64 chars, must match `^[a-z][a-z0-9_]*$`. Must be unique across your fields. This is the name the strategy and your escalation rules use. |
| `jsonPath` | 1–256 chars. See the supported syntax below.                                                                                                |
| `type`     | `STRING`, `NUMBER`, `BOOLEAN`, or `DATE`.                                                                                                   |

**Every field is optional.** A path that does not resolve yields `null` for that field and nothing else happens — the other fields are unaffected and the case proceeds. There is no way to declare a field mandatory, and a missing field is never an error.

### Supported path syntax

The path syntax is a **deliberately small subset** and is not general-purpose JSONPath. A path must match:

```text theme={null}
^\$(\.[a-zA-Z_][a-zA-Z0-9_]*|\[\d+\])*$
```

In practice, this means only two operations, starting from `$`:

* **Dot access** on identifier-shaped keys — `$.plan.name`
* **Numeric array indexing** — `$.seats[0].email`

<Warning>
  Wildcards (`$.items[*]`), filter expressions (`$.items[?(@.active)]`), recursive descent (`$..name`), slices, and bracket-quoted keys (`$['my-key']`) are **not supported** and will be rejected.

  A key that is not identifier-shaped — `total-revenue`, `2fa_enabled` — is unreachable. Rename it in your response, or expose an alias.
</Warning>

### Type coercion

The declared `type` controls how the extracted value is coerced. Anything that cannot be coerced becomes `null`.

| Type      | Behaviour                                                                                                                                     |
| --------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `STRING`  | Coerced to string.                                                                                                                            |
| `NUMBER`  | Non-finite values (`NaN`, `Infinity`) become `null`.                                                                                          |
| `BOOLEAN` | Accepts `true`, `false`, `"true"`, `"false"`, `1`, and `0`. **Any other value becomes `null`** — including `"yes"`, `"1"`, and `null` itself. |
| `DATE`    | **Kept as-is, as a string.** It is not parsed, validated, or normalised. An invalid date reaches the model verbatim.                          |

## Where enriched data is used

Once snapshotted, your fields feed three consumers:

<CardGroup cols={2}>
  <Card title="Recovery strategy" icon="brain">
    Fields are passed to the model that designs the recovery plan, as a `name → value` map. They influence channel ordering, touchpoint count, and timing.
  </Card>

  <Card title="Message generation" icon="envelope">
    The same context is available to the model that writes each message.
  </Card>

  <Card title="Escalation rules" icon="bolt">
    Fields are addressable as `enrichment.<field_name>` in escalation conditions — for example `enrichment.plan`.
  </Card>

  <Card title="Language resolution" icon="language">
    One field can be nominated to carry the customer's language. See below.
  </Card>
</CardGroup>

<Warning>
  Enriched values are passed to the model as context. They are **not** template variables — there is no `{{enrichment.field}}` interpolation, and you cannot force a value to appear verbatim in a message.
</Warning>

### Language field

You can nominate one of your fields as the language field. Its `name` must match an existing field name.

Airdun resolves a customer's language in this order, taking the first that yields a value:

1. The language declared on the customer in Airdun
2. Stripe's `preferred_locales`
3. **Your nominated enrichment field**
4. A country-to-language mapping
5. The workspace fallback language

## Example

Your endpoint:

```http theme={null}
GET /airdun/customers/cus_QxYz123 HTTP/1.1
Host: api.your-app.com
Authorization: Bearer sk_live_xxx
```

Your response:

```json theme={null}
{
  "plan": "scale",
  "locale": "fr",
  "team_size": 24,
  "has_paid_seat": true,
  "revenue_ltv_usd": 18400.5,
  "signed_up_at": "2024-03-11T09:12:00Z",
  "owner": { "email": "ada@acme.com" }
}
```

A matching field mapping:

| Name              | Path                | Type      |
| ----------------- | ------------------- | --------- |
| `plan`            | `$.plan`            | `STRING`  |
| `locale`          | `$.locale`          | `STRING`  |
| `team_size`       | `$.team_size`       | `NUMBER`  |
| `has_paid_seat`   | `$.has_paid_seat`   | `BOOLEAN` |
| `revenue_ltv_usd` | `$.revenue_ltv_usd` | `NUMBER`  |
| `signed_up_at`    | `$.signed_up_at`    | `DATE`    |
| `owner_email`     | `$.owner.email`     | `STRING`  |

With `locale` nominated as the language field, and an escalation rule on `enrichment.revenue_ltv_usd`.

## Testing and observability

Use **Test endpoint** in the dashboard to fire a real request against your endpoint with a customer ID of your choice. This performs an actual call and shows you the raw response alongside the projected fields — the fastest way to check a path or an auth header.

Every call Airdun makes is recorded, including failures, with its status, duration, and error category (`timeout`, `network`, `http_4xx`, `http_5xx`, `http_other`). Review them in the call log in your dashboard.

<Note>
  Recovery cases fired from the playground do **not** call your endpoint — the customer is synthetic, so the lookup would be meaningless. The snapshot for those cases is whatever the playground supplies.
</Note>

## Implementation notes

* **Respond fast.** The default budget is 3 seconds with no retry. Serve from a cache or a precomputed table rather than doing expensive joins on the request path.
* **Handle unknown IDs.** Stripe customer IDs may exist that your system has no record of. Return `2xx` with the fields you have rather than a 404 — a non-2xx discards the entire snapshot, including fields you could have supplied.
* **Keep keys identifier-shaped** so the path syntax can reach them.
* **Do not gate on the customer ID alone.** It travels in the URL and is not a secret. Use `BEARER` or `API_KEY`.
