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

# Decision export

> Send every policy decision to a webhook or an S3-compatible bucket.

Antara can deliver your organization's [policy decisions](https://docs.antarah.dev/guides/policies.md#decisions) to your own systems. Set up an integration in the console's **Integrations** page:

| Type | Delivery |
| - | - |
| HTTP webhook | `POST` to your HTTPS URL with a JSON body, signed with your signing secret |
| S3-compatible | Gzipped NDJSON files written to your bucket under `{prefix}/yyyy/mm/dd/hh/{first_id}_{last_id}.ndjson.gz` (UTC hour of the first event) |

Decisions arrive about a minute after they happen, in order, in batches of up to 1,000. They're content-free: no prompts, outputs, or judge reasoning.

## The decision object

```json theme={null}
{
  "object": "list",
  "data": [{
    "id": "dec_0193a6c2f1027bb4c3d2e1f0a9b8c7d6",
    "object": "organization.decision",
    "request_id": "req_0193a6c2f0017aa4b2c1d0e9f8a7b6c5",
    "project_id": "proj_0193a6c2e0017aa4b2c1d0e9f8a7b6c5",
    "api_key_id": "key_0193a6c2e1117aa4b2c1d0e9f8a7b6c5",
    "model": "a1",
    "policy_id": "pol_0193a6c2e7b47c3d9f1e2a4b5c6d7e8f",
    "policy_name": "Protect credentials",
    "policy_version": 3,
    "direction": "response",
    "mode": "enforcing",
    "result": "not_matched",
    "action": "allowed",
    "check_status": "completed",
    "latency_ms": 412,
    "created_at": 1793750400
  }]
}
```

| Field | Values |
| - | - |
| `result` | `matched`, `not_matched`, or `null` when the check didn't complete |
| `action` | `allowed`, `flagged` (monitoring match), `blocked` (enforcing match, or an enforcing check that failed) |
| `check_status` | `completed`, `failed`, `timed_out` |
| `api_key_id` | `null` for Playground requests |

## Verifying webhook signatures

Each webhook request has an `Antara-Signature` header:

```
Antara-Signature: t=1793750400,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
```

`v1` is the hex HMAC-SHA-256 of `t + "." + raw body`, keyed with your whole signing secret (`whsec_…`) as UTF-8. Compare in constant time and reject `t` older than 5 minutes.

```typescript Node.js theme={null}
import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(rawBody: string, header: string, secret: string): boolean {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=") as [string, string]));
  const t = Number(parts.t);
  if (!parts.v1 || !Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > 300) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return expected.length === parts.v1.length && timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}
```

```python Python theme={null}
import hashlib, hmac, time

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    t = int(parts.get("t", "0"))
    if "v1" not in parts or abs(time.time() - t) > 300:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])
```

Verify against the raw body bytes, before any JSON parsing. The signing secret is shown once, when you create the integration; to rotate it, create a new integration and delete the old one.

## Delivery rules

| Rule | Detail |
| - | - |
| Success | Any `2xx` within 10 seconds. Redirects aren't followed |
| Failure | Retried with exponential backoff, up to 1 hour apart. After 3 failures in a row the integration shows as **failing**; it recovers on the next success |
| Order | Batches are delivered in decision order; the next batch waits for the current one |
| Test | **Send test event** in the console delivers one synthetic decision (`policy_name` "Antara test event", IDs of zeros) through the same path and signature |
| Retention gap | Decisions are kept for 90 days. An integration paused longer skips what expired and reports `data_gap_<count>` |

Destinations must be public: Antara refuses private and internal network addresses.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.