Raposa Aval — API
One API call pauses a high-stakes action until an authorized human approves or rejects it. Every decision is sealed into a hash-chained audit log, and your systems are notified by a signed webhook.
Authentication
All endpoints take a bearer token. Two roles exist:
| Role | Can do |
|---|---|
client | create an approval, read its status |
operator | everything a client can, plus decide, list pending, verify the audit chain |
Authorization: Bearer <CLIENT_API_KEY>
Keys are issued by DC ESCRYPT and shown once — only their SHA-256 hashes are stored, and revocation takes effect immediately. Request one at raposa.group/start, or write to contact@raposa.group.
Create an approval
POST /api/v1/approvals
{
"action": "refund", // ≤ 200 chars, required
"context": "ticket 9182, EUR 240", // ≤ 4000 chars, required
"risk": "low | medium | high", // required
"requested_by": "agent-billing-1", // ≤ 200 chars, required
"expires_in_sec": 86400, // 60 … 2592000, default 86400
"webhook_url": "https://you.example/hook" // optional, https only
}
→ 200 {"id": "0f2c…", "status": "pending"}
Unknown fields are rejected — the schema is strict. Rate limit: 60 requests per minute per key; over the limit the API answers 429.
Read status
GET /api/v1/approvals/{id}
→ 200 {
"id": "0f2c…",
"action": "refund",
"context": "ticket 9182, EUR 240",
"risk": "low",
"requested_by": "agent-billing-1",
"status": "pending | approved | rejected | expired",
"created_at": "2026-08-24T09:12:03.114Z",
"expires_at": "2026-08-25T09:12:03.114Z",
"decided_by": "operator-anna",
"decided_at": "2026-08-24T09:14:41.882Z",
"decision_comment": "confirmed with the customer",
"webhook_url": "https://you.example/hook"
}
Expiry is evaluated on read: a pending approval past expires_at flips to expired, and that transition is audited and delivered by webhook like any other decision.
Decide (operator)
POST /api/v1/approvals/{id}/decision
Authorization: Bearer <OPERATOR_KEY>
{"decision": "approve | reject", "comment": "optional, ≤ 2000 chars"}
A second decision on the same approval returns 409. Humans can also decide in the operator panel at /api/panel.
List approvals (operator)
GET /api/v1/approvals?status=pending&risk=high&limit=50&offset=0
→ 200 {"approvals": [ … ], "total": 128, "limit": 50, "offset": 0}
status is one of pending, approved, rejected, expired or all; risk is optional; limit is capped at 200. Approvals past their expiry are flipped to expired as the list is read, so a listing never shows a stale pending row.
Verify the audit chain (operator)
GET /api/v1/audit/verify
→ 200 {"ok": true, "entries": 128}
→ 200 {"ok": false, "entries": 41, "broken_id": 42}
Each entry is sha256(prev_hash + ts + event_type + approval_id + actor + payload). Any edit, insert or delete breaks verification from that point on, so tampering is detectable rather than merely discouraged. Secrets never enter the payload.
Export your audit trail
GET /api/v1/audit/export
Authorization: Bearer <CLIENT_API_KEY>
→ 200 {
"count": 2,
"entries": [
{"id": 128, "ts": "2026-08-24T09:12:03.114Z", "event_type": "approval_created",
"approval_id": "0f2c…", "actor": "acme", "payload": {…},
"prev_hash": "9f1c…", "hash": "4ad0…", "self_hash_ok": true}
]
}
Returns the audit entries for your approvals only, each with its position in the global chain and a recomputed self_hash_ok. You cannot recompute the entire chain from this — that would require other customers' payloads, which isolation forbids; full-chain verification is the operator endpoint /v1/audit/verify.
Webhooks
If you pass webhook_url, the decision is POSTed to it:
X-Raposa-Event: approval.decided
X-Raposa-Delivery: <uuid, same across retries>
X-Raposa-Timestamp: 2026-08-24T09:14:41.913Z
X-Raposa-Signature: sha256=<hmac>
{"approval_id":"0f2c…","status":"approved","decided_by":"operator-anna",
"decided_at":"2026-08-24T09:14:41.882Z","comment":"confirmed with the customer"}
Verify the signature against the raw request body using your CLIENT_WEBHOOK_SECRET:
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
hmac.compare_digest(expected, request.headers["X-Raposa-Signature"])
Retries: immediately, then after 5 s, 30 s and 120 s — four attempts total. Success is any 2xx; redirects count as failure and are not followed; one attempt times out after 10 s. Every attempt is recorded in the audit log.
Your endpoint must be public https. URLs resolving to loopback, private, link-local or reserved addresses are rejected at creation time with 422 — this protects our infrastructure and yours.
Rotate your webhook secret
POST /api/v1/webhook-secret/rotate
Authorization: Bearer <CLIENT_API_KEY>
→ 200 {"webhook_secret": "…"} // shown once
The old secret stops working immediately. Deliveries already in flight were signed with it, so accept both values for a few minutes, or rotate during a quiet window.
Errors
| Code | When |
|---|---|
| 401 | missing, malformed, unknown or revoked key |
| 403 | client key used on an operator-only endpoint |
| 404 | no approval with that id |
| 409 | approval already approved, rejected or expired |
| 422 | schema violation, or an unusable webhook_url |
| 429 | more than 60 creations per minute for one key |
Minimal client
import requests, time
BASE = "https://dcescrypt.com/api"
H = {"Authorization": "Bearer " + CLIENT_API_KEY}
r = requests.post(f"{BASE}/v1/approvals", headers=H, json={
"action": "payout", "context": "invoice 7, EUR 1200",
"risk": "high", "requested_by": "agent-finance",
"webhook_url": "https://you.example/hooks/raposa",
}).json()
while True: # or just wait for the webhook
s = requests.get(f"{BASE}/v1/approvals/{r['id']}", headers=H).json()
if s["status"] != "pending":
break
time.sleep(5)
if s["status"] == "approved":
do_the_payout()
Data and retention
Servers and database are in Germany; transactional email goes through the EU region. What we store, on what legal basis and for how long is set out in the Privacy Policy. Do not put special-category personal data into context.
raposa