Agency API & Webhooks
REST API for white-label agencies to manage clients, subscriptions and wallet, plus signed webhooks for your CRM/billing. Machine-readable spec: OpenAPI 3.1.
Base URL & authentication
All endpoints live under https://www.saintaura.co/api/agency/v1 (also reachable on your panel host). Create a scoped key in the panel under API & webhooks and send it as a bearer token:
curl https://www.saintaura.co/api/agency/v1/clients \ -H "Authorization: Bearer sa_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Money is in integer minor units of your agency currency (e.g. cents, paise). Keys carry scopes; a request without the required scope returns 403 insufficient_scope.
Endpoints
| Method | Path | Scope | Description |
|---|---|---|---|
| GET | /clients | clients:read | List clients (filters: status, tier, ref, q; cursor pagination). |
| POST | /clients | clients:write | Create a client. Body: name, category, tier, cycle, client_ref, retail_price, brand_override, languages, google_place_id. Every client activates immediately and debits your agency wallet; trials are not available. |
| GET | /clients/{id} | clients:read | Client detail + stats_30d. |
| POST | /clients/{id}/subscription/change-plan | subscriptions:write | Change tier/cycle (entitlements immediate, billed next renewal). |
| POST | /clients/{id}/subscription/cancel | subscriptions:write | Cancel at period end. |
| POST | /clients/{id}/subscription/resume | subscriptions:write | Resume a halted subscription (debits the current period). |
| GET | /wallet | wallet:read | Wallet balance, currency, runway_days, lots. |
| GET | /wallet/ledger | wallet:read | Paginated ledger entries. |
| GET | /prices | clients:read | Wholesale price table + offered tiers. |
| GET | /events | wallet:read / reviews:read | Paginated event log (same payloads as webhooks). |
Pagination, idempotency & rate limits
- List endpoints accept ?cursor= and ?limit= (max 100) and return { data: [], next_cursor }.
- Every write accepts an Idempotency-Key header (≤128 chars, 24 h). A replay with the same key + body returns the stored response; a reuse with a different body returns 409 idempotency_mismatch.
- Rate limits: 600 requests/min/key, 60 writes/min/key. Exceeding returns 429 with a Retry-After header.
- Errors use { error: { code, message, details? } }. Codes include unauthorized, insufficient_scope, not_found, validation_failed, wallet_insufficient, slug_taken, tier_not_offered, rate_limited.
Webhooks
Register HTTPS endpoints in the panel. We POST JSON with a 10 s timeout and these headers: X-SaintAura-Signature: t=<unix>,v1=<hex>, X-SaintAura-Event, X-SaintAura-Delivery. Payload:
{
"id": "evt_...",
"type": "client.created",
"api_version": "v1",
"agency_id": "ag_...",
"created_at": "2026-06-01T00:00:00.000Z",
"data": { ... }
} The signature is HMAC-SHA256(secret, "<t>.<rawBody>"). During a secret rotation the header carries two v1= values (old + new) for 24 h — accept either. Retries run at 1 m, 5 m, 30 m, 2 h, 12 h, 24 h (7 attempts) then the delivery is marked dead. Endpoints resolving to private/loopback addresses, or non-HTTPS URLs, are rejected. Verify signatures:
Node.js
const crypto = require("crypto");
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");
// header may carry two v1= values during a key rotation; accept either
const sigs = header.split(",").filter(p => p.startsWith("v1=")).map(p => p.slice(3));
return sigs.some(s => crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)));
} Python
import hmac, hashlib
def verify(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body,
hashlib.sha256).hexdigest()
sigs = [p[3:] for p in header.split(",") if p.startswith("v1=")]
return any(hmac.compare_digest(s, expected) for s in sigs) PHP
<?php
function verify(string $rawBody, string $header, string $secret): bool {
$parts = [];
foreach (explode(",", $header) as $p) { [$k,$v] = explode("=", $p, 2); $parts[$k][] = $v; }
$expected = hash_hmac("sha256", $parts["t"][0] . "." . $rawBody, $secret);
foreach ($parts["v1"] ?? [] as $sig) if (hash_equals($sig, $expected)) return true;
return false;
}