WEBHOOKS
Two systems: events you receive, and a rule you own.
Outbound events tell your systems what happened: register an endpoint and APPEN POSTs a signed event at each step of the intent lifecycle. webhook_check runs the other direction — it is the fourteenth rule kind, and it makes your own HTTPS endpoint part of the decision, before any credential or money exists. Both use the same signature, so you write one verifier. No payload ever carries a card number; cards appear as a handle plus last four.
Register an endpoint
Endpoint registration is a human-owner action. The URL is checked against private, loopback, link-local, and cloud-metadata addresses at registration and again on every call. The signing secret is returned once.
POST /webhook-endpoints { "url": "https://hooks.example.com/appen", "label": "prod" }
→ 201 { "id": "whe_…", "secret": "whsec_…" } # save it — shown once
GET /webhook-endpoints # never returns the secret
DELETE /webhook-endpoints/{id} # revokeVerify a signature
Every POST carries X-APPEN-Signature: t=<unix>,v1=<hex>, an HMAC-SHA256 of "<t>.<body>" with your endpoint secret. Reject stale timestamps to stop replay.
import hashlib, hmac, time
def verify(secret: str, body: bytes, sig_header: str, tolerance_s: int = 300) -> bool:
parts = dict(kv.split("=", 1) for kv in sig_header.split(","))
if abs(time.time() - int(parts["t"])) > tolerance_s:
return False
expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + body,
hashlib.sha256).hexdigest()
return hmac.compare_digest(parts["v1"], expected)Outbound events — live today
Delivery is best-effort with a durable record: each event writes a delivery row you can audit, then POSTs once. Envelope: { id, type, created, data }, where data is the intent with its per-rule trace. Dedupe on id.
Designed, not live — these ship when card issuance goes live with our issuing partner:
webhook_check — your own rule
Create a rule with kind: webhook_check and APPEN POSTs each declared intent to your endpoint before deciding. You return { "decision": "approve" | "decline" | "review" }. The default is fail-closed: a slow or unreachable endpoint declines the spend — it cannot wave it through. Local rules run first, so an intent a local rule already declined never reaches your server.
POST /rules
{
"kind": "webhook_check",
"config": { "endpoint_id": "whe_…", "fail_mode": "closed" }
}
# your endpoint receives the PAN-free intent, HMAC-signed, and answers:
{ "decision": "decline", "reason": "vendor not in tonight's run" }WHERE THIS RUNS TODAY
The rule engine decides every declared intent today — live in the API, the MCP server, and the demo. Enforcement on real card swipes begins at our BIN connection with our issuing partner, when the same pre-authorized, user-directed policies execute inside the network authorization step. Card issuance opens by invite once our issuing-partner sandbox is live.