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.

Claude ChatGPT View as Markdown

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 SourcesNew 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 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 below for what this request contains.
  3. Use the source in a workflow-initiated session. Go to Workflows and open an existing workflow, then SettingsEdit, 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 SourcesYour sourceSettingsEdit, 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"
    }
  }
}
FieldDescription
typeAlways askpilot.webhook.verification. Use it to tell this request apart from payload requests.
organization_idThe ID of your organization in Askpilot.
source_idThe ID of the Webhook source. It stays the same for the life of the source.
data.target_urlThe URL you entered, exactly as you entered it.
data.signing_secretThe signing secret you entered, or null if you did not enter one.
data.body_exampleThe 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 secretThis 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:

Header
X-Askpilot-Token: base64("<timestamp>.<signature>")
PartDescription
timestampUnix time in seconds at which Askpilot created the request, for example 1756684800.
signatureHMAC-SHA256 of the timestamp string, using your signing secret as the key, encoded as lowercase hex (64 characters).

Decoded, a token looks like this:

Decoded token
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.

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"))

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 "", 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.

ValueTest data
Signing secretaskpilot-test-signing-secret
Timestamp1756684800
Signature6722b23f75ef85d2ab5e17e4aff11a0cc3e311f931b52004eef9f6d23c855a96
TokenMTc1NjY4NDgwMC42NzIyYjIzZjc1ZWY4NWQyYWI1ZTE3ZTRhZmYxMWEwY2MzZTMxMWY5MzFiNTIwMDRlZWY5ZjZkMjNjODU1YTk2
This token will fail the age checkIts 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())
curl
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 SourcesYour sourceSettingsEdit, enter a new signing secret and click Verify and save. Askpilot verifies the endpoint again and signs every later request with the new secret.