# Webhooks

On this page you can learn how to send data from a workflow-initiated session to your own systems with a Webhook source.

## How webhooks work

A Webhook source allows you to send data from a workflow-initiated session to your own systems. You create a source of the Webhook type in Askpilot and give it your endpoint URL, an optional signing secret and an example of the request body your endpoint expects. From then on the Ask agent can use the source in any workflow-initiated session. Whenever the workflow’s instructions call for it, the Ask agent builds a payload that follows your example and sends it to your endpoint. Your endpoint can be anything that accepts an HTTP `POST`, such as your own app, a CRM, a serverless function, an automation tool or any other system.

## Set up a webhook

Setting up a webhook takes three steps, all of them in Askpilot.

1. **Create a Webhook source in Askpilot.** In the Askpilot app, go to **Sources** → **New source**, choose the type **Webhook** and give it a name and a description. The Ask agent reads the name and the description to understand when to use the source, and the name is what you will see when you select sources for a workflow.
2. **Set up the webhook.** Once the Webhook source from step 1 exists, open it and click **Set up** to connect your webhook. The Set up Webhook dialog asks for three things.
   - **Endpoint URL.** This is the URL Askpilot posts to. It must be reachable from the internet over `https://` or `http://`. Askpilot does not follow redirects, so enter the final URL. It also refuses URLs that point to a private network or to `localhost`, and any URL longer than 2,000 characters.
   - **Signing secret.** This is optional, but we strongly recommend it. Askpilot signs every request it sends you with this secret, so your endpoint can check that the request came from Askpilot. Use a long random value and keep it private. Without a secret, anyone who knows your URL can post to it. See [Verify requests from Askpilot](#verify) below.
   - **Example request body.** Paste a JSON object that shows the exact shape you want to receive, with the field names, the nesting and the value types. The Ask agent models every payload on this example, so make it complete and realistic. It must be a JSON object, not a list or a single value.
   Then click **Verify and save**. Askpilot sends one test request to your URL to check that your endpoint is reachable, and **your endpoint must answer with a 2xx status code within 10 seconds**. If it does, the webhook is successfully set up. If it answers with any other status code, or does not answer in time, nothing is saved and Askpilot shows you an error, so you can fix the endpoint and try again. See [The verification request](#verification-request) below for what this request contains.
3. **Use the source in a workflow-initiated session.** Go to **Workflows** and open an existing workflow, then **Settings** → **Edit**, or create a new workflow. In the **Sources** field, select your Webhook source by its name. To be able to select sources, you need to add a trigger to the workflow first. Then, in the workflow’s description, tell the Ask agent when to use the source and refer to it by its name, for example “Once you have collected all the customer’s details, send them using the Quote source”, where Quote is the name of your source. From then on the Ask agent sends data to your endpoint whenever the workflow’s instructions call for it.

To update any value of the Webhook source later, go to **Sources** → Your source → **Settings** → **Edit**, change the fields you need and click **Verify and save**. Askpilot verifies the endpoint again and replaces the stored values.

## The verification request

The verification request is a `POST` with `Content-Type: application/json`. Its body is **not** your example payload. It is an envelope that tells your endpoint what is happening and hands it everything you entered:

```json
{
  "type": "askpilot.webhook.verification",
  "organization_id": "<your organization ID>",
  "source_id": "<the ID of the Webhook source in Askpilot>",
  "data": {
    "target_url": "https://example.com/webhooks/askpilot",
    "signing_secret": "<the signing secret you entered, or null>",
    "body_example": {
      "event": "order.created",
      "order_id": "123"
    }
  }
}
```

| Field | Description |
| --- | --- |
| `type` | Always `askpilot.webhook.verification`. Use it to tell this request apart from payload requests. |
| `organization_id` | The ID of your organization in Askpilot. |
| `source_id` | The ID of the Webhook source. It stays the same for the life of the source. |
| `data.target_url` | The URL you entered, exactly as you entered it. |
| `data.signing_secret` | The signing secret you entered, or `null` if you did not enter one. |
| `data.body_example` | The example payload you entered. |

If you entered a signing secret, the request also carries the `X-Askpilot-Token` header, signed with that secret, so you can verify it the same way as every later request.

> **The only request that carries your secret** This is the only request that ever contains your signing secret. That lets an endpoint that manages its own configuration store the secret automatically. If you store the secret by hand, you can ignore the field.

Your endpoint only has to do two things with the verification request:

- **Answer with a 2xx status code.** A `200` or a `204` both work. That is all Askpilot needs to set up the webhook.
- **Do not process it as workflow data.** Check the `type` field first. Payload requests have the shape of your example instead.

## Payload requests

Once the webhook is set up, the Ask agent sends payloads to your endpoint whenever the workflow’s instructions call for it. Each one is a `POST` with `Content-Type: application/json`, and its body follows the structure of your example request body. With the example above, a payload could look like this:

```json
{
  "event": "order.created",
  "order_id": "A-1042"
}
```

This is how Askpilot handles payload requests:

- **Any 2xx status code means accepted.** Any other status code counts as a rejection. The Ask agent is told that the payload was not accepted, so it can correct the payload and try again.
- **Askpilot waits up to 30 seconds for your answer.** If your endpoint does not answer in time, or cannot be reached, the Ask agent is told that the request failed. Askpilot never resends a payload on its own. The Ask agent decides whether to try again.
- **Redirects are not followed.** A 3xx answer counts as a rejection.
- **Every delivery is logged.** The source’s **Logs** tab lists each request with the status your endpoint returned.
- **Your answer can talk back to the Ask agent.** If your endpoint answers with a 2xx status code and a short single-line body of up to 300 characters, the Ask agent can read that text. For example, answer with the ID your system assigned to the record, and the Ask agent can report it in the session.

## Verify requests from Askpilot

When you have entered a signing secret, every request Askpilot sends to your endpoint carries the `X-Askpilot-Token` header. Verify this header to make sure that the request really came from Askpilot and was sent recently. If you did not enter a secret, the header is not sent.

### The X-Askpilot-Token header

The header value is a Base64-encoded string made of two parts separated by a dot:

```text
X-Askpilot-Token: base64("<timestamp>.<signature>")
```

| Part | Description |
| --- | --- |
| `timestamp` | Unix time in seconds at which Askpilot created the request, for example `1756684800`. |
| `signature` | HMAC-SHA256 of the timestamp string, using your signing secret as the key, encoded as lowercase hex (64 characters). |

Decoded, a token looks like this:

```text
1756684800.6722b23f75ef85d2ab5e17e4aff11a0cc3e311f931b52004eef9f6d23c855a96
```

Askpilot creates a new token for every request. A token is never reused.

### How to check a token

1. **Read the `X-Askpilot-Token` header.** If it is missing, reject the request.
2. **Decode and split.** Base64-decode the value and split it at the first dot into the timestamp and the signature.
3. **Check the timestamp.** Reject the request if the timestamp is more than 5 minutes (300 seconds) away from your server's current time. This limits the time in which a captured token could be replayed.
4. **Compute the expected signature.** Compute HMAC-SHA256 over the timestamp string, exactly as received, using your signing secret as the key. Encode the result as lowercase hex.
5. **Compare in constant time.** Compare your result with the signature from the header using a constant-time comparison. If they match, the request is authentic.

Answer with a 2xx status code only after the request passes these checks. Answer with `401 Unauthorized` otherwise.

### Verify a token in code

Here is the same check written in Python and in Node.js. Both examples use only the standard library, so there is nothing to install.

*Python*
```python
import base64
import hashlib
import hmac
import time

MAX_AGE_SECONDS = 300


def verify_askpilot_token(token: str, signing_secret: str) -> bool:
    """Return True if the X-Askpilot-Token header value is valid."""
    try:
        timestamp, signature = base64.b64decode(token).decode("utf-8").split(".", 1)
        sent_at = int(timestamp)
    except (TypeError, ValueError):
        return False

    if abs(time.time() - sent_at) > MAX_AGE_SECONDS:
        return False

    expected = hmac.new(
        signing_secret.encode("utf-8"),
        timestamp.encode("utf-8"),
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected.encode("utf-8"), signature.encode("utf-8"))
```

*Node.js*
```javascript
const crypto = require("node:crypto");

const MAX_AGE_SECONDS = 300;

function verifyAskpilotToken(token, signingSecret) {
  if (typeof token !== "string") return false;

  const decoded = Buffer.from(token, "base64").toString("utf8");
  const dot = decoded.indexOf(".");
  if (dot === -1) return false;

  const timestamp = decoded.slice(0, dot);
  const signature = decoded.slice(dot + 1);

  const sentAt = Number.parseInt(timestamp, 10);
  if (!Number.isFinite(sentAt)) return false;
  if (Math.abs(Date.now() / 1000 - sentAt) > MAX_AGE_SECONDS) return false;

  const expected = crypto
    .createHmac("sha256", signingSecret)
    .update(timestamp, "utf8")
    .digest("hex");

  const a = Buffer.from(expected, "utf8");
  const b = Buffer.from(signature, "utf8");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
```

Here is how to use that check in an endpoint that handles both the verification request and payload requests, with Flask or Express:

*Python (Flask)*
```python
from flask import Flask, abort, request

app = Flask(__name__)
SIGNING_SECRET = "your-signing-secret"  # load this from your configuration


@app.post("/webhooks/askpilot")
def askpilot_webhook():
    token = request.headers.get("X-Askpilot-Token", "")
    if not verify_askpilot_token(token, SIGNING_SECRET):
        abort(401)

    payload = request.get_json()
    if payload.get("type") == "askpilot.webhook.verification":
        return "", 204  # Askpilot checks the endpoint.

    # Handle the payload here.
    return "", 204
```

*Node.js (Express)*
```javascript
const express = require("express");

const app = express();
const SIGNING_SECRET = process.env.ASKPILOT_SIGNING_SECRET;

app.post("/webhooks/askpilot", express.json(), (req, res) => {
  if (!verifyAskpilotToken(req.get("X-Askpilot-Token"), SIGNING_SECRET)) {
    return res.sendStatus(401);
  }

  if (req.body.type === "askpilot.webhook.verification") {
    return res.sendStatus(204); // Askpilot checks the endpoint.
  }

  // Handle req.body here.
  res.sendStatus(204);
});
```

### Test your implementation

Use these values to check your code. The signature and the token were produced from the secret and the timestamp shown.

| Value | Test data |
| --- | --- |
| Signing secret | `askpilot-test-signing-secret` |
| Timestamp | `1756684800` |
| Signature | `6722b23f75ef85d2ab5e17e4aff11a0cc3e311f931b52004eef9f6d23c855a96` |
| Token | `MTc1NjY4NDgwMC42NzIyYjIzZjc1ZWY4NWQyYWI1ZTE3ZTRhZmYxMWEwY2MzZTMxMWY5MzFiNTIwMDRlZWY5ZjZkMjNjODU1YTk2` |

> **This token will fail the age check** Its timestamp is 1 September 2025, 00:00:00 UTC, so the 5-minute check in your code will reject it. When you run this test, skip the age check or set your test’s current time to that timestamp.

To test your endpoint end to end before you set up the webhook in Askpilot, mint a fresh token with your own secret and send yourself a verification request:

```python
import base64, hashlib, hmac, time

secret = "your-signing-secret"
timestamp = str(int(time.time()))
signature = hmac.new(secret.encode(), timestamp.encode(), hashlib.sha256).hexdigest()
print(base64.b64encode(f"{timestamp}.{signature}".encode()).decode())
```

```bash
curl -X POST https://example.com/webhooks/askpilot \
  -H "Content-Type: application/json" \
  -H "X-Askpilot-Token: <the token printed above>" \
  -d '{"type": "askpilot.webhook.verification", "organization_id": "test", "source_id": "test", "data": {"target_url": "https://example.com/webhooks/askpilot", "signing_secret": "your-signing-secret", "body_example": {"event": "order.created", "order_id": "123"}}}'
```

Your endpoint should answer with a 2xx status code. Send the same request without the header, or with a token older than 5 minutes, and it should answer with `401`.

## Security recommendations

- **Use HTTPS.** The signature covers the timestamp, not the request body. It proves that the request came from a party that knows your secret and that it was created recently, but it cannot detect a body changed in transit. TLS protects the body, and it also protects the signing secret that the verification request carries.
- **Use a strong secret.** Generate at least 32 random bytes with a cryptographically secure generator and encode them, for example as hex or Base64.
- **Keep the secret private.** Store it in your configuration or secrets vault, never in URLs, logs, version control or client-side code.
- **Compare in constant time.** Use `hmac.compare_digest`, `crypto.timingSafeEqual` or the equivalent in your language. Never compare signatures with `==`.
- **Keep your clock accurate.** Sync your server clock with NTP so that valid tokens are not rejected by the 5-minute window.
- **Rotate the secret when you need to.** Go to **Sources** → Your source → **Settings** → **Edit**, enter a new signing secret and click **Verify and save**. Askpilot verifies the endpoint again and signs every later request with the new secret.

---
This is the Markdown version of https://askpilot.com/docs/developers/webhooks.html. The whole documentation as one file: https://askpilot.com/docs/llms-full.txt
