Developers

UnfoldCFO API & Webhooks

A versioned REST API for grants, people, plans, awards, Section 16, ASC 718, and tax data — plus HMAC-signed webhooks for real-time events. Everything is tenant-scoped to your API key.

Machine-readable spec: /api/v1/openapi.json

Authentication

Create a personal API key in Settings → Security (shown once). Send it as a Bearer token on every request. Keys are scoped to your active company — all data you read or write belongs to that tenant.

curl https://www.unfoldcfo.com/api/v1/grants \
  -H "Authorization: Bearer ue_live_xxxxxxxxxxxx"

Base URL, versioning & rate limits

Response envelope

Success responses wrap data + meta; errors use a consistent error object.

// success
{ "data": { ... }, "meta": { "request_id": "...", "version": "v1", "timestamp": "..." } }

// error
{ "error": { "code": "validation_failed", "message": "...", "details": { ... }, "request_id": "..." } }

Endpoints

MethodPathDescription
GET/api/v1/grantsList grants (filter by state, plan_id, recipient_id, dates)
POST/api/v1/grantsPropose a grant (full compliance + approval workflow)
GET/api/v1/grants/{id}Get a single grant
GET/api/v1/peopleList people (filter by classification, insider)
POST/api/v1/peopleCreate a person
GET/api/v1/plansList active plans
GET/api/v1/plans/{id}Get a plan + its sub-buckets
GET/api/v1/bucketCurrent bucket-balance roll-up
GET/api/v1/awardsList awards (filter by status, plan_id)
GET/api/v1/awards/{id}Get an award + vest schedule + issuance events
GET/api/v1/section-16/reviewsList Section 16 reviews
GET/api/v1/section-16/reviews/{id}Get a Section 16 review
GET/api/v1/section-16/filingsList filed Form 3/4/5
GET/api/v1/section-16/filings/{id}Get a filing (includes form_4_xml)
GET/api/v1/asc-718/expenseASC 718 expense rows (period_start, period_end)
GET/api/v1/tax-eventsTaxable equity events (year)
GET/api/v1/audit-logImmutable audit trail (super_admin keys)
GET/api/v1/webhooksList webhook subscriptions (super_admin)
POST/api/v1/webhooksCreate a subscription (secret shown once)
GET/api/v1/webhooks/deliveriesRecent webhook deliveries (debugging)

Webhooks

Subscribe an https endpoint to events in Settings → Webhooks. Each delivery is a JSON POST signed with HMAC-SHA256. Retries use exponential backoff (2, 4, 8, 16, 32 minutes); 10 consecutive failures auto-disable the endpoint.

Event types

grant.proposed — Grant proposed
grant.approved — Grant approved (all approvers cleared)
grant.granted — Grant granted (award created)
grant.rejected — Grant rejected
grant.withdrawn — Grant withdrawn
award.issued — Shares issued
award.vested — Shares vested
section_16.review_created — Section 16 review created
section_16.filing_completed — Section 16 filing completed
document.generated — Document generated
83b.deadline_approaching — 83(b) election deadline approaching
webhook.test — Test event (sent from the dashboard)

Delivery headers

Sample payload

{
  "event_id": "b3f1c2a4-...-...",
  "event_type": "grant.granted",
  "timestamp": "2026-05-28T14:03:11.482Z",
  "company_id": "1111....-1111-1111-1111-............",
  "data": {
    "grant_id": "....",
    "award_id": "....",
    "recipient_id": "....",
    "shares": 12500,
    "award_type": "RSU"
  }
}

Verify the signature — Node.js

import crypto from "node:crypto";

// Express example. Capture the RAW body (do not re-serialize before verifying).
function verify(rawBody, signatureHeader, secret) {
  const expected = "sha256=" + crypto
    .createHmac("sha256", secret)
    .update(rawBody, "utf8")
    .digest("hex");
  const a = Buffer.from(signatureHeader);
  const b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

app.post("/webhooks/ue", express.raw({ type: "application/json" }), (req, res) => {
  const sig = req.get("X-UE-Signature");
  if (!verify(req.body, sig, process.env.UE_WEBHOOK_SECRET)) {
    return res.status(401).send("bad signature");
  }
  const event = JSON.parse(req.body);
  // event.event_id is an idempotency key — dedupe on it.
  res.sendStatus(200);
});

Verify the signature — Python

import hmac, hashlib

def verify(raw_body: bytes, signature_header: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret.encode(), raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature_header or "")

# Flask example
@app.post("/webhooks/ue")
def ue_webhook():
    if not verify(request.get_data(), request.headers.get("X-UE-Signature"), UE_WEBHOOK_SECRET):
        return ("bad signature", 401)
    event = request.get_json()
    # event["event_id"] is an idempotency key — dedupe on it.
    return ("", 200)

Machine-readable reference

The complete OpenAPI 3.0 specification is served at /api/v1/openapi.json. Import it into Postman, Insomnia, Scalar, or any OpenAPI client to generate an SDK or an interactive explorer. (We serve the raw spec rather than embedding a third-party renderer to keep the Content-Security-Policy strict — no external script origins.)