Handle events
Instead of polling, get events pushed to your server the moment they happen. Register an endpoint in the console; AgentPay sends a signed POST for each event and retries until your server answers 2xx.
Register an endpoint
In the console open Developer, Webhooks and add an https URL. Pick the events it should receive, or leave the list empty to receive all of them. The signing secret is shown once, at creation: store it next to your API key.
The delivery
Each delivery is one JSON object.
{
"id": "evt_checkout_submitted_run_7a1e...",
"type": "checkout.submitted",
"created": 1762182000,
"data": {
"checkout_id": "run_7a1e...",
"order_id": "123456789",
"order_total": "1.98 USD"
}
}| Header | Value |
|---|---|
Joomjoo-Signature | t=<unix seconds>,v1=<hex HMAC-SHA256> |
Joomjoo-Event-Type | The event type, e.g. checkout.submitted. |
Joomjoo-Event-Id | The event id, the same as id in the body. |
Each header is also sent under its older Mooj- name with the same value, so integrations written before the rename keep verifying.
Events
See events for every event and its data fields.
Verify the signature
Recompute the HMAC over t + "." + rawBody with your endpoint's signing secret and compare it to v1 in constant time. Use the raw request body, exactly as received, not a re-serialised object.
import crypto from "node:crypto";
export function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const expected = crypto.createHmac("sha256", secret).update(parts.t + "." + rawBody).digest("hex");
return expected.length === parts.v1.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}
// Express: keep the raw body
app.post("/agentpay/events", express.raw({ type: "application/json" }), (req, res) => {
if (!verify(req.body.toString(), req.get("Joomjoo-Signature"), process.env.AGENTPAY_WEBHOOK_SECRET)) return res.sendStatus(400);
const event = JSON.parse(req.body);
// handle event.type, event.data
res.sendStatus(200);
});import hmac, hashlib
def verify(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
msg = parts["t"].encode() + b"." + raw_body
expected = hmac.new(secret.encode(), msg, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"])Reject anything that does not verify, and treat t older than a few minutes as stale.
Retries and ordering
- Your endpoint must answer with a 2xx status within the delivery timeout. Anything else, or a network error, is retried with backoff.
- A paused or deleted endpoint, and an event it is not subscribed to, is skipped and not retried.
- Deliveries can arrive out of order and, after a retry, more than once. Use
idto deduplicate, and read the current state fromGET /v1/checkout/{id}orGET /v1/spend/{id}when it matters. - The console's Developer, Webhooks page lists every delivery with its status code and attempt count.