Developers
Webhooks
On this page you can learn how to send data from a workflow-initiated session to your own systems with a Webhook source.
Learn this page in your AI agent
Open it in Claude or ChatGPT and ask anything about it, or copy the Markdown and use it in any AI agent.
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.
- 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.
- 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://orhttp://. Askpilot does not follow redirects, so enter the final URL. It also refuses URLs that point to a private network or tolocalhost, 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 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.
- Endpoint URL. This is the URL Askpilot posts to. It must be reachable from the internet over
- 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:
{
"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.
Your endpoint only has to do two things with the verification request:
- Answer with a 2xx status code. A
200or a204both work. That is all Askpilot needs to set up the webhook. - Do not process it as workflow data. Check the
typefield 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:
{
"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:
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:
1756684800.6722b23f75ef85d2ab5e17e4aff11a0cc3e311f931b52004eef9f6d23c855a96Askpilot creates a new token for every request. A token is never reused.
How to check a token
- Read the
X-Askpilot-Tokenheader. If it is missing, reject the request. - Decode and split. Base64-decode the value and split it at the first dot into the timestamp and the signature.
- 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.
- 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.
- 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.
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"))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:
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 "", 204const 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 |
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:
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())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.timingSafeEqualor 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.