Skip to content
AXOWLDevelopers
Quickstart

Webhooks

Two directions, and they are completely separate systems:

DirectionMeaningWhere
OutboundAxowl → your server/api/org/{slug}/webhook-endpoints (register)
InboundA provider → Axowl/api/v1/webhooks/…

Register an HTTPS endpoint, subscribe it to event names, and Axowl POSTs a signed JSON envelope when those events happen.

POST /api/org/{slug}/webhook-endpoints
Content-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 · RouteScopePurpose
GET /webhook-endpointsorg.setting.readList: id, url, description, isActive, failureCount, lastSuccessAt, lastFailureAt, events, disabledByFailures. Never includes the secret
POST /webhook-endpointsorg.setting.updateCreate → returns secret once
PUT /webhook-endpoints/{id}org.setting.updateChange URL / events / description (also clears the failure counter of an active endpoint)
POST /webhook-endpoints/{id}/rotate-secretorg.setting.updateNew secret, returned once as { "secret": "whsec_…" } → old signatures stop verifying immediately
POST /webhook-endpoints/{id}/toggleorg.setting.updateEnable / disable with { "isActive": true | false } (enabling also clears the failure counter)
DELETE /webhook-endpoints/{id}org.setting.updateDelete 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.

events is a list of event names. Axowl publishes one outbound event:

EventFires when
agent.wakeSomething 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.

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

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: 1800000000
webhook-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.

  • 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.wake loses 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 with POST /webhook-endpoints/{id}/toggle ({"isActive": true}), which also resets the counter. A single success resets it too. Changing an active endpoint with PUT clears the counter. On an endpoint the circuit breaker disabled, PUT keeps the counter and disabledByFailures, 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.

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 · RouteSenderPurpose
POST /{provider}/brand/{token}Your app group’s own payment gateway (portone, razorpay)Payments, subscription charges and refunds for that brand’s store
POST /portonePortOneKorean real-name verification results; one-time KRW payments to Axowl
POST /stripeStripe IdentityIdentity-check results

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 answered duplicate and recorded once.
  • Retry request — 503 when 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.

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 finalize call does not arrive: on an IdentityVerification.Verified event Axowl completes the verification that the same user started with initiate. 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.Paid Axowl 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 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.

EventEffect
identity.verification_session.verifiedThe account’s identity check becomes Verified and the person is notified
identity.verification_session.requires_inputStatus RequiresInput, with Stripe’s reason; the person is asked to try again
identity.verification_session.canceledStatus Canceled

An event for a session that is not the account’s current one is ignored.