Webhooks
Guides

Webhooks

Receive Asks events on your server in real time — event types, signed deliveries, verification, and retries.

Webhooks Business and up push events to your server as they happen — a new message, an escalation, a captured lead — so you can react without polling. Deliveries are signed, retried on failure, and logged so you can inspect and replay them.

Create an endpoint

Open Integrations → Webhooks

Go to app.asks.app/integrations/webhooks and select Add Endpoint. Only workspace owners and admins can manage endpoints.

Enter the URL and pick events

Enter the Endpoint URL (HTTPS only) and an optional description. Keep All events checked, or uncheck it and tick the events you want — the list is every event type below.

Integrations → Webhooks with the Add Webhook Endpoint dialog open: the Endpoint URL field, the optional Description field, and the Events list with All events unchecked and individual event checkboxes visible
Add an endpoint and choose which events it receives.
Copy the signing secret

Select Create Endpoint. The Signing Secret (whsec_…) is shown once — store it where your server can read it.

You can also create endpoints with the API, using the webhooks:manage scope:

Terminal
curl https://api.asks.app/v1/webhook_endpoints \
  -H "Authorization: Bearer $ASKS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/asks/webhooks",
    "enabled_events": ["lead.captured", "data.collected", "agent.escalated"]
  }'

The response includes the signing secret (whsec_…) exactly once — store it securely. The URL must be HTTPS and is validated against internal, loopback, and metadata addresses at registration and again at every delivery.

Event types

Subscribe to specific events, or ["*"] for all:

EventFires when
conversation.createdA conversation is created.
conversation.updatedA conversation changes (status, assignment, …).
conversation.closedA conversation is closed.
message.createdA message is added to a conversation.
customer.createdA customer is created.
customer.updatedA customer is updated.
lead.capturedA lead's contact details are captured — by the agent in conversation or through a Collect Leads form.
data.collectedA Collect Data action is completed.
ticket.createdA ticket is created.
ticket.updatedA ticket is updated.
ticket.resolvedA ticket is resolved.
sla.at_riskA ticket approaches an SLA target.
sla.breachedA ticket misses an SLA target.
agent.escalatedThe AI agent escalates to a human.
order.createdA Commerce order is created — by the AI agent, a team member or the API.
order.paidAn order's payment is confirmed: a verified Xendit notification, a check with Xendit, or a bank transfer marked paid.
order.payment_expiredAn order's payment link expired unpaid; its reserved stock was released.
order.cancelledAn order is cancelled.
order.updatedAn order's status or tracking number changes, a new payment link replaces an expired one, the unpaid order is edited, a payment proof is received or rejected, or a refund is recorded.
order.needs_reviewA payment needs a person: the amount didn't match, the order was paid twice, or it was paid after it was cancelled.

data.collected carries submissionId, actionId, actionName, conversationId, customerId, channel, source (form or conversation), the collected data as field–value pairs, and completedAt. lead.captured carries the updated customer with the captured fields — or, when a lead form only repeats details already on record, the same submission shape as data.collected without the action fields.

The sla.* events fire when SLA tracking is enabled for the workspace. Their data is the ticket resource plus two extra fields: sla_metric (first_response, next_response, or resolution) and sla_due_at (the target that was at risk or missed).

The order.* events fire only in workspaces with Commerce (Indonesian stores that don't use Shopify). Their data is the order as GET /v1/orders returns it when the event fires, plus extra fields: payment_channel and payment_source (webhook, reconcile or manual) on order.paid; review_reason (amount_mismatch, double_payment or paid_after_cancel) and, for an amount mismatch, paid_amount on order.needs_review; and changes on order.updated: status_changed, tracking_updated, payment_renewed, edited (with the old and new totals), payment_proof_received, payment_proof_rejected (with the reason) or refund_recorded (with the amount and the new payment status). Amounts are whole rupiah.

The endpoint's Send test event button in the dashboard (or POST /webhook_endpoints/{id}/test) sends a synthetic ping event.

Deliveries

Events arrive as a POST with a JSON envelope; data carries the event's payload (for message.created, a message object shaped like the REST resource):

JSON
{
  "id": "cmc1a2b3…",
  "type": "message.created",
  "created_at": "2026-07-06T12:00:00.000Z",
  "data": { "object": "message", "id": "cmc1a2b4…" }
}
HeaderValue
Asks-Signaturet=<unix>,v1=<hmac> — verify this (below).
Asks-Event-IdThe event id (matches id in the body).
Asks-Event-TypeThe event type.
Asks-Webhook-IdThe delivery id.
User-AgentAsks-Webhooks/1.0

Respond with any 2xx within 10 seconds to acknowledge. Anything else counts as a failure.

Verify the signature

The Asks-Signature header is Stripe-style: t=<unix timestamp>,v1=<hex HMAC>, where the HMAC is HMAC-SHA256(secret, "{t}.{raw body}"). Verify against the raw request body and reject deliveries older than 300 seconds:

JavaScript
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyAsksSignature(rawBody, header, secret, tolerance = 300) {
  const parts = Object.fromEntries(
    header.split(",").map((kv) => kv.split("=")),
  );
  const t = Number(parts.t);
  if (!t || !parts.v1) return false;
  if (Math.abs(Date.now() / 1000 - t) > tolerance) return false;

  const expected = createHmac("sha256", secret)
    .update(`${t}.${rawBody}`)
    .digest("hex");
  const a = Buffer.from(parts.v1, "hex");
  const b = Buffer.from(expected, "hex");
  return a.length === b.length && timingSafeEqual(a, b);
}

Compute the HMAC over the exact raw body bytes. Parsing and re-serializing the JSON changes the bytes and breaks verification — read the raw body before your framework's JSON middleware touches it.

Retries and auto-disable

  • Failed deliveries retry with exponential backoff starting at 1 minute — up to 8 attempts per delivery.
  • After 15 consecutive failed deliveries, the endpoint is automatically disabled and the workspace's owners and admins are notified by email. Re-enable it with the endpoint's switch in the dashboard, or with PATCH /webhook_endpoints/{id} and "status": "enabled" — either way the failure counter resets.
  • The endpoint's View deliveries button shows the last 20 attempts with their HTTP status. Through the API, list them with GET /webhook_endpoints/{id}/deliveries and replay one with POST /webhook_endpoints/{id}/deliveries/{deliveryId}/retry.

Roll the secret

If a signing secret leaks, the endpoint's Roll signing secret button (or POST /webhook_endpoints/{id}/roll_secret) generates a new one, shown once. The old secret stops working immediately, so update your verifier in the same deploy.

Event log

An event is recorded whenever at least one enabled endpoint subscribes to its type — whether or not the delivery succeeded. Events no endpoint listens for are not stored. Browse the log with GET /events. All management endpoints are documented in the Webhook endpoints reference.

Chat on WhatsApp