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

# Webhooks

Slate webhooks push event data to your server the moment something happens in our system, so you never need to poll for updates. When a subscribed event occurs, Slate sends an HTTP `POST` request to your registered endpoint with a JSON payload describing the event.

## Getting started

<Steps>
  <Step title="Build a webhook endpoint">
    Create an HTTPS endpoint on your server that accepts `POST` requests and returns a `200 OK` response. The endpoint must be publicly reachable and support TLS. Return a `2xx` before performing any slow processing. If there is additional processing needed, use a queue internally if needed.
  </Step>

  <Step title="Register your endpoint with Slate">
    Send your endpoint URL to your Slate integration contact. We'll provision your webhook and return a HMAC-SHA256 **signing secret** used to verify that every delivery came from Slate.
  </Step>

  <Step title="Verify signatures and process events">
    On each incoming request, verify the `svix-signature` header using your signing secret before trusting the payload. Then act on the event type and matter data in the body.
  </Step>
</Steps>

## Receiving events

Slate delivers every event as an HTTP `POST` to your registered endpoint.

### Request headers

| **Header**       | **Description**                                                                                                                              |
| :--------------- | :------------------------------------------------------------------------------------------------------------------------------------------- |
| `svix-id`        | Unique identifier for this message. Stable across retry attempts — use it for idempotency.                                                   |
| `svix-timestamp` | Unix timestamp (seconds) of when the message was sent. Reject messages with timestamps older than 5 minutes to guard against replay attacks. |
| `svix-signature` | One or more HMAC-SHA256 signatures of the payload, space-separated, each prefixed with `v1`. See Verifying signatures below.                 |
| `content-type`   | `application/json`                                                                                                                           |

### Example delivery

```text theme={null}
POST /webhooks/slate HTTP/1.1
Host:            your-server.example.com
Content-Type:    application/json
svix-id:         msg_2uBSWQ3FJk9XrNqVeLDkTm3E
svix-timestamp:  1725897600
svix-signature:  v1,7q5h2Kz3mNpR1vLwXoYeAcBdFgHiJkMn4s6t8u0=

{
  "crid": "644333222",
  "downloadUrl": "https://slate.inc/download_url.pdf",
  "fileRequestUUID": "e845435f-13a6-4c46-889a-ea6b27414494",
  "fileUUID": "d1ea8f8d-b07a-47f9-b02e-bbba960cf1e0",
  "matterUUID": "d2a64dba-a8bf-45be-ab91-424b4a99bd29"
}
```

### Responding to deliveries

Return any HTTP `2xx` status within **30 seconds**. Any non-`2xx` response or timeout triggers a retry.

## Event types

Each webhook delivery carries an event type that identifies what happened.

### `DOCUMENT_READY`

Fired when a signed or processed document becomes available on a matter.

```json theme={null}
{
  "crid": "644333222",
  "downloadUrl": "https://slate.inc/download_url.pdf",
  "fileRequestUUID": "e845435f-13a6-4c46-889a-ea6b27414494",
  "fileUUID": "d1ea8f8d-b07a-47f9-b02e-bbba960cf1e0",
  "matterUUID": "d2a64dba-a8bf-45be-ab91-424b4a99bd29"
}
```

Additional event types will be added over time. Your Slate contact will notify you when new events become available for your integration.

## Payload reference

| **Field**         | **Type** | **Description**                                                                                                        |
| :---------------- | :------- | :--------------------------------------------------------------------------------------------------------------------- |
| `matterUUID`      | string   | UUID of the matter this event relates to. Use this to correlate the event with a record in your system.                |
| `crid`            | string   | The account reference number (CRID) assigned to this matter.                                                           |
| `downloadUrl`     | string   | The presigned download url of the signed pdf document. The embedded authentication in the url is valid for 60 minutes. |
| `fileRequestUUID` | string   | Unique UUID of the process in which the document was signed.                                                           |
| `fileUUID`        | string   | Unique UUID of the signed document. Can be used to distinguish signed documents.                                       |

Slate follows an **additive-only** policy: existing fields will not be removed or renamed in a breaking way. Build your handler to ignore unknown fields.

## Verifying signatures

Always verify the signature before processing a payload. This confirms the request came from Slate and hasn't been tampered with.

### How it works

Slate constructs a signed string from three components, then signs it with HMAC-SHA256 using your signing secret that will be shared at the time your endpoint is registered with Slate:

```text theme={null}
signed_content = "{svix-id}.{svix-timestamp}.{raw request body}"
signature      = Base64( HMAC-SHA256( signing_secret, signed_content ) )
```

The `svix-signature` header may contain multiple signatures (space-separated). A request is valid if any one of them matches — this supports secret rotation without downtime. Compare `svix-timestamp` to the current time and discard the request if the difference exceeds 5 minutes.

### Using the Svix SDK (recommended)

Slate's webhook delivery is powered by [**Svix**](https://www.svix.com/), and Svix publishes official SDKs in Python, Node.js, Go, Java, Ruby, PHP, Rust, and more. The SDK handles signature verification, timestamp checking, and secret decoding in a single call.

```python theme={null}
from svix.webhooks import Webhook, WebhookVerificationError

SIGNING_SECRET = "whsec_..."  # provided by Slate

def handle_webhook(headers: dict, body: bytes):
    wh = Webhook(SIGNING_SECRET)
    try:
        payload = wh.verify(body, headers)
    except WebhookVerificationError:
        return 400  # invalid signature

    matter_uuid = payload["matterUUID"]
    crid = payload["crid"]
    # ... process the event
    return 200
```

Install: `pip install svix`

### Manual verification — Python

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

SIGNING_SECRET = "whsec_..."  # provided by Slate

def verify_signature(headers, body: str):
    msg_id    = headers["svix-id"]
    timestamp = headers["svix-timestamp"]
    sigs      = headers["svix-signature"]

    # Reject requests older than 5 minutes
    if abs(time.time() - int(timestamp)) > 300:
        raise ValueError("Timestamp too old")

    # Decode the secret (strip the "whsec_" prefix, then base64-decode)
    secret_bytes = base64.b64decode(SIGNING_SECRET.removeprefix("whsec_"))

    signed_content = f"{msg_id}.{timestamp}.{body}"
    expected = base64.b64encode(
        hmac.new(secret_bytes, signed_content.encode(), hashlib.sha256).digest()
    ).decode()

    # svix-signature: "v1,<sig1> v1,<sig2> ..."
    received = [s.split(",", 1)[1] for s in sigs.split() if s.startswith("v1,")]
    if expected not in received:
        raise ValueError("Invalid signature")
```

### Manual verification — Node.js

```javascript theme={null}
const crypto = require("crypto");

const SIGNING_SECRET = "whsec_..."; // provided by Slate

function verifySignature(headers, rawBody) {
  const msgId     = headers["svix-id"];
  const timestamp = headers["svix-timestamp"];
  const sigs      = headers["svix-signature"];

  // Reject stale timestamps (>5 min)
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
    throw new Error("Timestamp too old");
  }

  // Decode the secret (strip "whsec_" prefix, then base64-decode)
  const secretBytes = Buffer.from(
    SIGNING_SECRET.replace(/^whsec_/, ""), "base64"
  );

  const signedContent = `${msgId}.${timestamp}.${rawBody}`;
  const expected = crypto
    .createHmac("sha256", secretBytes)
    .update(signedContent)
    .digest("base64");

  const received = sigs
    .split(" ")
    .filter(s => s.startsWith("v1,"))
    .map(s => s.slice(3));

  if (!received.includes(expected)) {
    throw new Error("Invalid signature");
  }
}
```

## Retries and delivery

If your endpoint doesn't return a `2xx` within 30 seconds, Slate retries with exponential backoff:

| **Attempt** | **Delay**  |
| :---------- | :--------- |
| 2           | 5 seconds  |
| 3           | 1 minute   |
| 4           | 5 minutes  |
| 5           | 30 minutes |
| 6           | 2 hours    |
| 7           | 5 hours    |
| 8           | 10 hours   |
| 9           | 24 hours   |

After 9 attempts the message is marked failed. Contact your Slate integration contact if you need a missed event replayed.

### Idempotency

Because a delivery may be attempted more than once, your handler should be **idempotent**. Processing the same event twice should produce the same result as processing it once. Use the `svix-id` header as a stable unique key to deduplicate incoming events.

### Ordering

Events are delivered in order on a best-effort basis. Retries and network conditions can cause reordering, so design your handler to tolerate out-of-order events if strict ordering matters to your use case.
