Webhooks and notifications
One event list, delivered as signed webhooks on every plan and emailed to owners where a person should know.
Arettic has one list of events. Each event is sent to your webhook endpoints, signed, and retried for 24 hours until your server answers 2xx. Some are also emailed to the org's owners. Webhooks are on every plan, and so is the list of recent events.
Add an endpoint
POST https://api.arettic.com/v1/orgs/{orgId}/webhooks as an owner (or with a write org key), or on the Webhooks page of the dashboard. Send url, and optionally events: a list of event types to receive. No events, or an empty list, means every event.
- The answer carries the endpoint's signing secret (
whsec_…) once. Store it now; it isn't shown again. It is kept encrypted with your org's key. - In production the URL must be
httpsand a public address (not a private or local network).http://localhostworks while you develop against a local Arettic. - Up to 10 endpoints per org; the 11th gets
402 plan_limit. DELETE /v1/orgs/{orgId}/webhooks/{id}removes one;GET /v1/orgs/{orgId}/webhookslists them with the event types you can pick.
curl https://api.arettic.com/v1/orgs/$ORG_ID/webhooks \
-H "Authorization: Bearer $ARETTIC_ORG_KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://example.com/arettic-hook", "events": ["job.completed", "dispute.decided"] }'{
"webhook": {
"id": "0b8e2c4d-6f1a-4e3b-9c5d-7e8f9a0b1c2d",
"secret": "whsec_4f1c…",
"url": "https://example.com/arettic-hook",
"events": ["job.completed", "dispute.decided"]
},
"note": "Store the secret now: it isn't shown again."
}Events
| Type | When | `data` fields | Emailed to owners |
|---|---|---|---|
balance.low | The org's balance fell under its low-balance alert (default $5; set it with PUT /v1/orgs/{orgId}/notifications and low_balance_usd). Once per top-up. | balance_credits, balance_usd | yes |
budget.80pct | A live agent has spent 80% of its monthly budget. Once per agent per month. | agent_id, agent_name, spent_credits, budget_credits | yes |
approval.requested | A purchase is waiting for an owner's approval. | approval_id, agent_id, tool_id, item_count, amount_credits, amount_usd, reason (over_threshold or over_budget), expires_at | no (owners get their own email with a one-click link) |
approval.decided | An owner approved or rejected it. | approval_id, agent_id, status (approved or rejected), note | no |
job.completed | A job (more than 25 items) finished. | job_id, status, receipt_id, item_count, passed, expired | no |
tool.paused | A tool your org used in the last 30 days was paused. | tool_id, tool_name, reason | yes |
dispute.decided | One of your disputes was decided. | dispute_id, receipt_id, item_index, decision (upheld or rejected), refunded_credits, note | yes |
price.changed | The price per success of a tool your org used in the last 30 days changed. | tool_id, tool_name, old_credits, new_credits, valid_from | yes |
trial.expiring | Trial credits expire within 7 days. Once per grant. | credits, expires_on | yes |
webhook.test | You asked for a test delivery (below). | message | no |
GET https://api.arettic.com/v1/orgs/{orgId}/events lists recent events, whether or not you have endpoints: a way to catch up after downtime.
The delivery
Each delivery is a POST with a JSON body and these headers:
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | Arettic-Webhooks/1.0 |
Arettic-Event-Id | The event's id: the same on every retry, so you can drop duplicates. |
Arettic-Event-Type | The event type, e.g. job.completed. |
Arettic-Signature | t=<unix seconds>,v1=<hex HMAC-SHA256> (below). |
{
"id": "6a1d3f5b-2c4e-4d6f-8a1b-3c5d7e9f1a2b",
"type": "job.completed",
"created_at": "2026-09-30T11:04:52.000Z",
"data": {
"job_id": "d3b07384-d9a0-4c9b-8f1e-2a5c7e9b1d3f",
"status": "done",
"receipt_id": "9e107d9d-372b-4b6a-8a1f-3c5d7e9f1a2b",
"item_count": 200,
"passed": 181,
"expired": 0
}
}{
"id": "1f2e3d4c-5b6a-4978-8a9b-0c1d2e3f4a5b",
"type": "approval.requested",
"created_at": "2026-09-30T11:10:00.000Z",
"data": {
"approval_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"agent_id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
"tool_id": "peopledatalabs-enrich-company",
"item_count": 400,
"amount_credits": "24000",
"amount_usd": "24.000",
"reason": "over_threshold",
"expires_at": "2026-10-01T11:10:00.000Z"
}
}Verify the signature
Arettic-Signature is t=<unix seconds>,v1=<signature>, where the signature is the hex HMAC-SHA256, keyed with your endpoint's secret, of the string <t>.<raw body>. Verify it against the raw body, before parsing the JSON, and reject a t more than 5 minutes from your clock (it stops replays). Compare in constant time.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyArettic(secret: string, rawBody: string, header: string): boolean {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=") as [string, string]));
const t = Number(parts.t);
if (!t || !parts.v1 || Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1);
return a.length === b.length && timingSafeEqual(a, b);
}import hashlib
import hmac
import time
def verify_arettic(secret: str, raw_body: bytes, header: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
try:
t = int(parts["t"])
except (KeyError, ValueError):
return False
if "v1" not in parts or abs(time.time() - t) > 300:
return False
signed = f"{t}.".encode() + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"])Retries
Any 2xx answer within 10 seconds counts as delivered. Anything else (another status, a timeout, a redirect, a refused connection) is retried after 1, 5, 15, 30, 60, 120, 240, 480 and 480 minutes: ten tries over about 24 hours. After that, or once the next try would fall past 24 hours from the event, the delivery is marked failed. Redirects are never followed.
Deliveries can arrive out of order and, rarely, more than once. Use Arettic-Event-Id to drop duplicates, and the event's created_at or the object it points to (fetch the job, the receipt, the dispute) for the current state.
Test and debug
POST /v1/orgs/{orgId}/webhooks/{id}/testsends awebhook.testevent to that one endpoint straight away and answers with the delivery'sstatus,last_status_codeandlast_error.GET /v1/orgs/{orgId}/webhook-deliverieslists recent deliveries: status, attempts, the last status code or error, and when the next try is.- The Webhooks page of the dashboard shows both, and the SDKs have
org.webhooks.create,.list,.testand.delete.
Schema
The event envelope and every event's data are published as JSON Schema at /schemas/webhook-event.json.