Unfold Equity 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.unfoldingvalues.com/api/v1/grants \
-H "Authorization: Bearer ue_live_xxxxxxxxxxxx"Base URL, versioning & rate limits
- Base URL:
https://www.unfoldingvalues.com/api/v1 - Rate limit: 100 requests/minute per key (configurable). Exceeding it returns
429with aRetry-Afterheader. - Pagination:
limit(max 100, default 50) + opaquecursor; responses carrymeta.next_cursorandmeta.total_count. - Every response includes an
X-Request-Idheader (andmeta.request_id) — quote it when contacting support.
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
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/grants | List grants (filter by state, plan_id, recipient_id, dates) |
| POST | /api/v1/grants | Propose a grant (full compliance + approval workflow) |
| GET | /api/v1/grants/{id} | Get a single grant |
| GET | /api/v1/people | List people (filter by classification, insider) |
| POST | /api/v1/people | Create a person |
| GET | /api/v1/plans | List active plans |
| GET | /api/v1/plans/{id} | Get a plan + its sub-buckets |
| GET | /api/v1/bucket | Current bucket-balance roll-up |
| GET | /api/v1/awards | List awards (filter by status, plan_id) |
| GET | /api/v1/awards/{id} | Get an award + vest schedule + issuance events |
| GET | /api/v1/section-16/reviews | List Section 16 reviews |
| GET | /api/v1/section-16/reviews/{id} | Get a Section 16 review |
| GET | /api/v1/section-16/filings | List filed Form 3/4/5 |
| GET | /api/v1/section-16/filings/{id} | Get a filing (includes form_4_xml) |
| GET | /api/v1/asc-718/expense | ASC 718 expense rows (period_start, period_end) |
| GET | /api/v1/tax-events | Taxable equity events (year) |
| GET | /api/v1/audit-log | Immutable audit trail (super_admin keys) |
| GET | /api/v1/webhooks | List webhook subscriptions (super_admin) |
| POST | /api/v1/webhooks | Create a subscription (secret shown once) |
| GET | /api/v1/webhooks/deliveries | Recent 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 proposedgrant.approved — Grant approved (all approvers cleared)grant.granted — Grant granted (award created)grant.rejected — Grant rejectedgrant.withdrawn — Grant withdrawnaward.issued — Shares issuedaward.vested — Shares vestedsection_16.review_created — Section 16 review createdsection_16.filing_completed — Section 16 filing completeddocument.generated — Document generated83b.deadline_approaching — 83(b) election deadline approachingwebhook.test — Test event (sent from the dashboard)Delivery headers
X-UE-Signature:sha256=<hex>— HMAC-SHA256 of the raw body with your secretX-UE-Event: the event typeX-UE-Delivery-Id/X-UE-Event-Id: delivery + idempotency identifiers
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.)