Privacy · Terms

Raposa Aval — API

v0.1 · base URL https://dcescrypt.com/api · updated 2026-08-24

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:

RoleCan do
clientcreate an approval, read its status
operatoreverything 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

CodeWhen
401missing, malformed, unknown or revoked key
403client key used on an operator-only endpoint
404no approval with that id
409approval already approved, rejected or expired
422schema violation, or an unusable webhook_url
429more 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.