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

# Extract structured data

> Attach a schema to an inbox and incoming email arrives as validated, typed fields instead of raw text.

Attach a schema to an inbox and every incoming email arrives with the fields you asked for — an order id, a total, a tracking number — already extracted and validated. No parsing, no prompts, no regex on your side.

## Attach a schema

`PUT /v1/inboxes/{inbox}/schema` attaches or replaces the extraction contract, and needs the `inboxes:write` scope. Address the inbox by `id` or by its full address.

<CodeGroup>
  ```bash title="cURL" theme={null}
  curl -X PUT https://api.dairo.app/v1/inboxes/orders@yourapp.com/schema \
    -H "Authorization: Bearer $DAIRO_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
          "schema": {
            "orderId": { "type": "string", "required": true },
            "total": { "type": "number" }
          },
          "onValidationError": "quarantine",
          "extractionHint": "Pull the order ID and total from the confirmation."
        }'
  ```

  ```ts title="TypeScript" theme={null}
  const schema = await dairo.inboxes.setSchema("orders@yourapp.com", {
    schema: { orderId: { type: "string", required: true }, total: { type: "number" } },
    onValidationError: "quarantine",
    extractionHint: "Pull the order ID and total from the confirmation.",
  });
  ```

  ```python title="Python" theme={null}
  schema = dairo.inboxes.set_schema(
      "orders@yourapp.com",
      schema={"orderId": {"type": "string", "required": True}, "total": {"type": "number"}},
      on_validation_error="quarantine",
      extraction_hint="Pull the order ID and total from the confirmation.",
  )
  ```

  ```text title="MCP" theme={null}
  Tool: manage_inboxes   (scope inboxes:write, confirm required)
  Args: { "action": "setSchema", "inbox": "orders@yourapp.com",
          "schema": { "orderId": { "type": "string", "required": true } },
          "onValidationError": "quarantine", "confirm": true }
  ```
</CodeGroup>

```json theme={null}
{
  "object": "inbox_schema",
  "inboxId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "schema": {
    "orderId": { "type": "string", "required": true },
    "total": { "type": "number" }
  },
  "onValidationError": "quarantine",
  "extractionHint": "Pull the order ID and total from the confirmation.",
  "createdAt": "2026-06-12T10:00:00+00:00",
  "updatedAt": "2026-06-12T10:00:00+00:00"
}
```

### Schema shape

The schema is a flat map of field name to declaration. A standard JSON Schema also works — `{ "type": "object", "properties": { … }, "required": [ … ] }` is folded into the same shape on write.

* **Types:** `string`, `number`, `integer`, `boolean`, `url`, and `email` — the last two validate their format. `object` and `array` declare nested values.
* **Per-field keys:** `required`, `default`, `enum`, and `maxLength`, applied when an incoming email is validated against the schema.
* **Size:** the whole schema must stay within 8 KiB. An empty schema (`{}`, or omitted) means no contract — mail passes through unshaped.
* **`extractionHint`:** an optional plain-English pointer for the extractor, up to 1,000 characters. It is prompt context only, never executed. A `PUT` replaces the whole contract, so include the hint on every update you want to keep.

### Where the data lands

Once a schema is attached, each matching email carries its results on the message record — `extractionStatus` for the outcome and `structured` for the extracted object:

```json theme={null}
{
  "object": "message",
  "subject": "Order #4821 confirmed",
  "extractionStatus": "validated",
  "structured": { "orderId": "4821", "total": 129.5 }
}
```

The `message.received` [webhook](/webhooks/webhooks) event carries the same result in a `structured` block, so an event-driven consumer never has to fetch the body to get the fields.

Extracted values are copied from the email, never invented: a required field whose value doesn't actually appear in the message fails validation instead of being filled in with a plausible guess.

## When an email doesn't fit

`onValidationError` decides what happens when an email can't satisfy the schema — say, a confirmation with no order total.

| Value                  | What happens                                                                                                                                                                                 |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `quarantine` (default) | The message is stored but held out of your normal flow: no `message.received` event fires. A `message.quarantined` event fires instead, carrying the raw candidate and the validation error. |
| `passthrough`          | The message is delivered normally, carrying the raw candidate plus the validation error.                                                                                                     |

The outcome for any message is readable on its `extractionStatus`:

| Status        | Meaning                                                                                  |
| ------------- | ---------------------------------------------------------------------------------------- |
| `validated`   | The extraction matched the schema; `structured` holds the typed object.                  |
| `quarantined` | Validation failed under `quarantine`; the message was held aside.                        |
| `passthrough` | Validation failed under `passthrough`; the message was delivered with the raw candidate. |
| `skipped`     | Extraction didn't run — the inbox had no contract.                                       |
| `error`       | Extraction was unavailable; the message was delivered normally, without structured data. |

## Read or remove the schema

Check what contract an inbox carries, or detach it to go back to plain, unshaped mail. Reading uses the `inboxes:read` scope; detaching uses `inboxes:write`. Reading an inbox that has no schema returns `404`; detaching returns `204` with an empty body.

<CodeGroup>
  ```bash title="cURL" theme={null}
  curl https://api.dairo.app/v1/inboxes/orders@yourapp.com/schema \
    -H "Authorization: Bearer $DAIRO_API_KEY"

  curl -X DELETE https://api.dairo.app/v1/inboxes/orders@yourapp.com/schema \
    -H "Authorization: Bearer $DAIRO_API_KEY"
  ```

  ```ts title="TypeScript" theme={null}
  const schema = await dairo.inboxes.getSchema("orders@yourapp.com");
  await dairo.inboxes.deleteSchema("orders@yourapp.com"); // back to plain mail
  ```

  ```python title="Python" theme={null}
  schema = dairo.inboxes.get_schema("orders@yourapp.com")
  dairo.inboxes.delete_schema("orders@yourapp.com")
  ```
</CodeGroup>

<Tip>
  A structured inbox turns "parse the email" into "read the fields." Pair it with the `message.received` event: when a matching email arrives, the extracted data is already in the payload.
</Tip>

## Next steps

* [Inboxes](/receiving/inboxes) — create the inbox a schema attaches to.
* [Messages & threads](/receiving/messages-and-threads) — read the messages your schema shapes.
* [Wait for one-time codes](/receiving/verification-waits) — extract one value from one awaited email instead of every email.
