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

# Webhooks

> Verify signed resume callbacks from TryAgent.

# Webhooks

When a reviewer decides — or a policy timeout path applies — TryAgent sends a
signed `POST` to your escalation's `resume.url`. The SDK verifies the signature
for you so you never hand-roll HMAC.

## The signature

When `resume.secret` is set, TryAgent signs the exact JSON body with HMAC-SHA256
and sends:

* `x-tryagent-event`: `escalation.decided` for a reviewer decision, or
  `escalation.breached` when the SLA timeout applies. The `resolvedBy` field
  (`human` or `timeout`) carries the same distinction.
* `x-tryagent-delivery: <delivery id>`
* `x-tryagent-signature: v1=<hex hmac>`

Verification runs on Web Crypto, so it works unchanged on Node 20+, edge
runtimes, Deno, and Workers.

<Warning>
  Always pass the raw, unparsed request body. Re-serializing JSON changes the
  bytes and breaks the signature. Callbacks without `resume.secret` are unsigned —
  always set a secret in production.
</Warning>

## webhooks.constructEvent

Verifies the signature and returns the typed event. Throws
[`WebhookSignatureError`](/sdk/errors) when verification fails, so a returned
event is always safe to act on.

```typescript theme={null}
import { TryAgent, WebhookSignatureError } from "@tryagent/sdk";

const tryagent = new TryAgent({ apiKey: process.env.TRYAGENT_API_KEY! });

export async function POST(request: Request) {
  const body = await request.text();

  try {
    const event = await tryagent.webhooks.constructEvent({
      payload: body,
      signature: request.headers.get("x-tryagent-signature"),
      secret: process.env.TRYAGENT_WEBHOOK_SECRET!,
    });

    await resumeWorkflow(event.runId, {
      escalationId: event.escalationId,
      choice: event.choice,
      answeredBy: event.answeredBy,
      answeredAt: event.answeredAt,
    });

    return Response.json({ ok: true });
  } catch (error) {
    if (error instanceof WebhookSignatureError) {
      return Response.json({ error: "Invalid signature" }, { status: 401 });
    }
    throw error;
  }
}
```

## webhooks.verify

Returns a `boolean` instead of throwing or parsing — use it when you want to
branch on validity yourself.

```typescript theme={null}
const valid = await tryagent.webhooks.verify({
  payload: body,
  signature: request.headers.get("x-tryagent-signature"),
  secret: process.env.TRYAGENT_WEBHOOK_SECRET!,
});
```

Both methods are also exported as standalone functions, `constructWebhookEvent`
and `verifyWebhookSignature`, for use without a client instance.

## Event shape

`constructEvent` resolves to a `WebhookResumeEvent`:

| Field          | Type                   | Description                                                     |
| -------------- | ---------------------- | --------------------------------------------------------------- |
| `escalationId` | `string`               | The escalation that was resolved.                               |
| `runId`        | `string`               | The durable run handle to resume.                               |
| `agentId`      | `string`               | The agent that paused.                                          |
| `policy`       | `string`               | The policy key the escalation ran under.                        |
| `choice`       | `string \| null`       | The selected choice id. `null` when canceled.                   |
| `response`     | `object`               | Structured values when the escalation defined `responseFields`. |
| `resolvedBy`   | `"human" \| "timeout"` | Whether a reviewer decided or the SLA timeout applied.          |
| `answeredBy`   | `string \| null`       | The reviewer name, or `null` for a timeout.                     |
| `answeredAt`   | `string`               | ISO timestamp of the decision.                                  |
