Running an org

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.

curl
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"] }'
Response
{
  "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

Every event type and its data
TypeWhen`data` fieldsEmailed to owners
balance.lowThe 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_usdyes
budget.80pctA live agent has spent 80% of its monthly budget. Once per agent per month.agent_id, agent_name, spent_credits, budget_creditsyes
approval.requestedA 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_atno (owners get their own email with a one-click link)
approval.decidedAn owner approved or rejected it.approval_id, agent_id, status (approved or rejected), noteno
job.completedA job (more than 25 items) finished.job_id, status, receipt_id, item_count, passed, expiredno
tool.pausedA tool your org used in the last 30 days was paused.tool_id, tool_name, reasonyes
dispute.decidedOne of your disputes was decided.dispute_id, receipt_id, item_index, decision (upheld or rejected), refunded_credits, noteyes
price.changedThe price per success of a tool your org used in the last 30 days changed.tool_id, tool_name, old_credits, new_credits, valid_fromyes
trial.expiringTrial credits expire within 7 days. Once per grant.credits, expires_onyes
webhook.testYou asked for a test delivery (below).messageno

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:

Delivery headers
HeaderValue
Content-Typeapplication/json
User-AgentArettic-Webhooks/1.0
Arettic-Event-IdThe event's id: the same on every retry, so you can drop duplicates.
Arettic-Event-TypeThe event type, e.g. job.completed.
Arettic-Signaturet=<unix seconds>,v1=<hex HMAC-SHA256> (below).
job.completed (example values)
{
  "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
  }
}
approval.requested (example values)
{
  "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.

Node / TypeScript
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);
}
Python
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

Schema

The event envelope and every event's data are published as JSON Schema at /schemas/webhook-event.json.

Updated 2026-09-30 · This page as Markdown · JSON