Webhooks
Two directions, and they are completely separate systems:
| Direction | Meaning | Where |
|---|---|---|
| Outbound | Axowl → your server | /api/org/{slug}/webhook-endpoints (register) |
| Inbound | A provider → Axowl | /api/v1/webhooks/… |
Outbound — Axowl calls you
Section titled “Outbound — Axowl calls you”Register an HTTPS endpoint, subscribe it to event names, and Axowl POSTs a signed JSON envelope when those events happen.
Register an endpoint
Section titled “Register an endpoint”POST /api/org/{slug}/webhook-endpointsContent-Type: application/json
{ "url": "https://example.com/hooks/axowl", "events": ["agent.wake"], "description": "agent runner" }The response carries the signing secret once. No later call returns it again; rotate-secret
issues a new one, and returns that one once in the same way:
{ "id": "…", "secret": "whsec_…" }These routes need a signed-in member of the organization with the scope shown.
| Method · Route | Scope | Purpose |
|---|---|---|
GET /webhook-endpoints | org.setting.read | List: id, url, description, isActive, failureCount, lastSuccessAt, lastFailureAt, events, disabledByFailures. Never includes the secret |
POST /webhook-endpoints | org.setting.update | Create → returns secret once |
PUT /webhook-endpoints/{id} | org.setting.update | Change URL / events / description (also clears the failure counter of an active endpoint) |
POST /webhook-endpoints/{id}/rotate-secret | org.setting.update | New secret, returned once as { "secret": "whsec_…" } → old signatures stop verifying immediately |
POST /webhook-endpoints/{id}/toggle | org.setting.update | Enable / disable with { "isActive": true | false } (enabling also clears the failure counter) |
DELETE /webhook-endpoints/{id} | org.setting.update | Delete the endpoint |
Limits: 20 endpoints per organization. Create and update return 400 with the reason in error
when the URL is not allowed (see Delivery behavior), when no event is
subscribed, or when an event name is not one Axowl publishes.
Subscribing to events
Section titled “Subscribing to events”events is a list of event names. Axowl publishes one outbound event:
| Event | Fires when |
|---|---|
agent.wake | Something is waiting for one of the organization’s AI agents — an answer, an approval decision, an executed action, assigned or changed work. See MCP and Projects. |
Names are matched case-insensitively. Any other name is refused at registration with 400, and the
error lists the names you can subscribe to. An endpoint with no subscribed events is refused as well —
it could never fire.
The envelope
Section titled “The envelope”{ "id": "9f1c…", // unique per event "type": "agent.wake", // the event name you subscribed to "timestamp": "2027-01-02T03:04:05.0000000Z", "data": { } // event payload}agent.wake carries a pointer, not content: data holds agentConnectedId, reason, document
(a document number, when the wake is about an approval), next (a one-line instruction for the
agent), and for project work storyId, candidateIndex and candidateCount. What happened is read
by the agent itself through MCP.
Verifying the signature
Section titled “Verifying the signature”Signatures follow Standard Webhooks — the same scheme Stripe and Svix use, so an existing verification library works unchanged.
webhook-id: 4b7d… (unique per request; not the envelope id)webhook-timestamp: 1800000000webhook-signature: v1,<base64>signedContent = "{webhook-id}.{webhook-timestamp}.{raw body}"expected = base64( HMAC-SHA256( base64decode(secret minus "whsec_"), signedContent ) )Compare in constant time, and reject a webhook-timestamp more than 5 minutes from your clock
— that window is what stops a captured request from being replayed.
Delivery behavior
Section titled “Delivery behavior”- Timeout 10 seconds. Any non-2xx, timeout, or connection failure counts as a failure.
- No automatic retry. Treat delivery as at-most-once. A missed
agent.wakeloses nothing: the agent reads everything that is waiting for it the next time it connects through MCP. - Circuit breaker — after 20 consecutive failures the endpoint disables itself (the list shows
disabledByFailures: true). Re-enable it withPOST /webhook-endpoints/{id}/toggle({"isActive": true}), which also resets the counter. A single success resets it too. Changing an active endpoint withPUTclears the counter. On an endpoint the circuit breaker disabled,PUTkeeps the counter anddisabledByFailures, and does not switch the endpoint back on. - Egress is guarded — URLs that resolve to private, loopback, or cloud-metadata addresses are refused, at registration and again at send time (so DNS rebinding does not help).
- No secret, no send. If an endpoint’s signing secret is missing or malformed, Axowl skips the delivery rather than sending something you cannot distinguish from a forgery.
Inbound — providers call Axowl
Section titled “Inbound — providers call Axowl”Three routes under /api/v1/webhooks. Providers call them; your integration never does. The only
address you handle yourself is the per-brand payment URL, which you paste into your payment
gateway’s console.
| Method · Route | Sender | Purpose |
|---|---|---|
POST /{provider}/brand/{token} | Your app group’s own payment gateway (portone, razorpay) | Payments, subscription charges and refunds for that brand’s store |
POST /portone | PortOne | Korean real-name verification results; one-time KRW payments to Axowl |
POST /stripe | Stripe Identity | Identity-check results |
Per-brand payment gateway
Section titled “Per-brand payment gateway”Each connected gateway gets its own address, shown when you connect it — see
Applications. The {provider} in the path must be the gateway the
{token} was issued for.
- Signature — verified with that brand’s own webhook secret, using the gateway’s signing scheme.
A bad signature returns
401. - Unknown address — an unknown token, or a token used under another provider’s path, returns
404. - Result —
200 { "outcome": "…" }. The same event delivered twice is answeredduplicateand recorded once. - Retry request —
503when a refund arrives before its payment is recorded, or when the payment could not be confirmed with the gateway; the gateway’s redelivery completes it.
PortOne — Korea
Section titled “PortOne — Korea”Signatures use the Standard Webhooks scheme: webhook-id / webhook-timestamp /
webhook-signature headers, HMAC-SHA256, ±5-minute replay window. A missing or bad signature returns
401. After the signature passes, the answer is always 200.
- Real-name verification. Korean identity verification runs through PortOne. This webhook is the
safety net for when the client’s
finalizecall does not arrive: on anIdentityVerification.Verifiedevent Axowl completes the verification that the same user started withinitiate. It is idempotent, so it can race the client’s own call harmlessly. Other identity event types are ignored. - One-time KRW payments to Axowl (plan and add-on purchases). On
Transaction.PaidAxowl asks PortOne for the payment, checks status and amount, and then delivers the purchase — also when the browser closed before confirming. Delivery is idempotent.
Stripe Identity
Section titled “Stripe Identity”/stripe receives Stripe Identity events for the check started with
POST /api/v1/identity/kyc/start (see User account). It carries no payment
events. The Stripe-Signature header is verified; a bad signature returns 400.
| Event | Effect |
|---|---|
identity.verification_session.verified | The account’s identity check becomes Verified and the person is notified |
identity.verification_session.requires_input | Status RequiresInput, with Stripe’s reason; the person is asked to try again |
identity.verification_session.canceled | Status Canceled |
An event for a session that is not the account’s current one is ignored.