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

MethodPathScopeDescription
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

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;
}

Event types

client.createdclient.updatedclient.location_addedsubscription.activatedsubscription.renewedsubscription.renewal_failedsubscription.plan_changedsubscription.cancel_scheduledsubscription.cancelledsubscription.haltedsubscription.resumedwallet.creditedwallet.debitedwallet.low_balancewallet.lot_expiringwallet.lot_expiredreview.draftedreview.destination_clickedfeedback.negativedomain.verifieddomain.active