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

# Webhooks

> Receive events as they happen, and verify they came from Recepta.ai.

Webhooks push events to your endpoint as they occur — faster than polling, and they cost you no API requests.

## Events

| Event                   | Fires when                             |
| ----------------------- | -------------------------------------- |
| `call.started`          | A call connects                        |
| `call.ended`            | A call finishes                        |
| `call.transcript_ready` | The transcript for a call is available |
| `call.human_escalation` | A call is transferred to a person      |
| `sms.received`          | An inbound text arrives                |
| `sms.sent`              | An outbound text is sent               |
| `contact.created`       | A new contact is created               |
| `contact.updated`       | An existing contact changes            |

<Note>
  `call.ended` and `call.transcript_ready` are separate because transcription completes after the call does. If you need the transcript, act on `call.transcript_ready` — acting on `call.ended` and immediately fetching will often find nothing there yet.
</Note>

## Setting up an endpoint

<Steps>
  <Step title="Expose an HTTPS URL">
    It must accept `POST` with a JSON body and return a `2xx` quickly.
  </Step>

  <Step title="Register it">
    Add the endpoint in the dashboard and subscribe it to the events you want. Subscribe only to what you'll use.
  </Step>

  <Step title="Store the signing secret">
    You're given a secret prefixed `whsec_`. Keep it in your secret manager — you need it to verify deliveries.
  </Step>

  <Step title="Verify every delivery">
    See below. An unverified webhook endpoint accepts anything anyone posts to it.
  </Step>
</Steps>

## Payload

```json theme={null}
{
  "id": "evt_4f9c2a1e8b7d6c5a3e2f1b0d9c8a7b6e",
  "type": "call.ended",
  "created": "2026-08-02T14:32:07.412Z",
  "data": {}
}
```

`data` carries the event-specific detail. Treat unknown fields as additive — new ones may appear without notice, so parse defensively rather than rejecting on unexpected keys.

## Verifying signatures

Each delivery carries an `X-Webhook-Signature` header: the HMAC-SHA256 of the exact raw request body, keyed with your endpoint's signing secret, hex-encoded.

<CodeGroup>
  ```javascript Node.js theme={null}
  import crypto from 'crypto';

  function verify(rawBody, signature, secret) {
    const expected = crypto
      .createHmac('sha256', secret)
      .update(rawBody, 'utf8')
      .digest('hex');

    // Constant-time compare — never use ===
    const a = Buffer.from(expected, 'hex');
    const b = Buffer.from(signature, 'hex');
    return a.length === b.length && crypto.timingSafeEqual(a, b);
  }
  ```

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

  def verify(raw_body: bytes, signature: str, secret: str) -> bool:
      expected = hmac.new(
          secret.encode("utf-8"), raw_body, hashlib.sha256
      ).hexdigest()
      return hmac.compare_digest(expected, signature)
  ```

  ```php PHP theme={null}
  <?php
  function verify(string $rawBody, string $signature, string $secret): bool {
      $expected = hash_hmac('sha256', $rawBody, $secret);
      return hash_equals($expected, $signature);
  }
  ```
</CodeGroup>

<Warning>
  Compute the HMAC over the **raw request body**, byte for byte. Parsing the JSON and re-serializing it changes whitespace and key order, and the signature will never match. Capture the raw body before your framework parses it.
</Warning>

<Warning>
  Always compare in constant time — `timingSafeEqual`, `compare_digest`, `hash_equals`. A plain `==` leaks the expected signature to a patient attacker.
</Warning>

## Building a reliable consumer

<AccordionGroup>
  <Accordion title="Respond fast, work later" icon="bolt">
    Verify, enqueue, return `2xx`. Doing real work inline makes deliveries time out.
  </Accordion>

  <Accordion title="Be idempotent" icon="copy">
    Key on the event `id` and ignore ones you've already processed. Assume any event can arrive more than once.
  </Accordion>

  <Accordion title="Don't assume ordering" icon="arrow-down-a-z">
    Use the `created` timestamp rather than arrival order. `call.transcript_ready` may land before you've finished processing `call.ended`.
  </Accordion>

  <Accordion title="Reject unverified payloads" icon="shield">
    Return `401` and log it. A public endpoint accepting unsigned events is an open door into your systems.
  </Accordion>
</AccordionGroup>

## Testing

During development, point an endpoint at a request-inspection tool or an ngrok tunnel and trigger a real event — place a test call, or create a contact. Confirm you can verify the signature before writing any business logic on top.

<Card title="Deactivate rather than delete" icon="toggle-off">
  If a consumer is broken, deactivate the endpoint while you fix it. Deleting and re-creating issues a new signing secret you'd have to redeploy.
</Card>
