MyCFO API
CFO-grade metrics, forecasts, scenarios, and alerts — scoped to your organization. All amounts are in cents. All timestamps are ISO 8601 UTC.
Every request (except /v1/auth/register and /v1/auth/login)
requires a bearer token issued at registration or login.
Authorization: Bearer <access_token>
Tokens are short-lived JWTs. Their TTL is set by JWT_ACCESS_TTL_SECONDS
in your environment. All data is scoped to the organization that owns the token.
Every error response follows the same envelope.
{
"error": {
"type": "invalid_request",
"code": "missing_field",
"message": "name is required.",
"param": "name",
"request_id": "req_01j..."
}
}
| Status | When |
|---|---|
| 200 | Successful read |
| 201 | Resource created |
| 202 | Ingest accepted and completed |
| 204 | Successful delete (no body) |
| 400 | Malformed JSON |
| 401 | Missing or invalid bearer token |
| 404 | Resource not found (or not owned by your org) |
| 409 | Idempotency key reuse with different body |
| 422 | Validation error (missing required field, bad subtype, etc.) |
| 502 | Upstream dependency (e.g., Stripe) unavailable |
| 500 | Unexpected server error |
All list endpoints use cursor-based pagination.
| Parameter | Default | Description |
|---|---|---|
| limit | 20 | Max results to return (max 100) |
| starting_after | — | ID of the last item from the previous page |
Response shape for all lists:
{
"data": [ /* array of objects */ ],
"has_more": true
}
All state-mutating endpoints (POST, PATCH) support an idempotency key.
Send the same key with the same body and you get back the original response.
Sending the same key with a different body returns 409 conflict.
Idempotency-Key: my-unique-request-id-123
All metrics are computed over a rolling 30-day window ending on as_of.
| Field | Definition |
|---|---|
| gross_revenue_cents_30d | Sum of all revenue transactions (excluding refunds) in window |
| refunds_cents_30d | Sum of refund transactions (negative value) |
| net_revenue_cents_30d | gross_revenue + refunds |
| mrr_cents | Recurring revenue only — manual recurring events + Stripe subscription_invoice events |
| arr_cents | mrr_cents × 12 |
| burn_cents_30d | total_expenses - net_revenue |
| recurring_burn_cents_30d | recurring_expenses - net_revenue. Used for runway. |
| one_time_expenses_cents_30d | Sum of one_time_expense transactions |
| runway_months | cash_on_hand_cents / recurring_burn_cents_30d when burn is positive; otherwise null |
| cash_on_hand_cents | Set on the workspace via PATCH. Not derived from transactions. |
Creates a new organization and returns an access token.
{
"org_name": "Acme Inc",
"email": "ceo@acme.com",
"password": "hunter2"
}
{
"org": {
"id": "org_01j...",
"name": "Acme Inc",
"email": "ceo@acme.com",
"created_at": "2026-03-01T00:00:00+00:00"
},
"access_token": "eyJ..."
}
Returns an access token for an existing organization.
{
"email": "ceo@acme.com",
"password": "hunter2"
}
{
"org": {
"id": "org_01j...",
"name": "Acme Inc",
"email": "ceo@acme.com",
"created_at": "2026-03-01T00:00:00+00:00"
},
"access_token": "eyJ..."
}
A workspace represents one business or product. All transactions, metrics, forecasts,
scenarios, and alerts are scoped to a workspace.
cash_on_hand_cents drives runway calculations.
Creates a new workspace. Supports idempotency.
{
"name": "Acme Corp", // required
"cash_on_hand_cents": 5000000 // optional, in cents ($50,000)
}
{
"id": "ws_01j...",
"org_id": "org_01j...",
"name": "Acme Corp",
"cash_on_hand_cents": 5000000,
"created_at": "2026-03-01T00:00:00+00:00"
}
Lists all workspaces for your organization. Paginated.
limit=20&starting_after=ws_01j...
{
"data": [ /* workspace objects */ ],
"has_more": false
}
Returns a single workspace.
{
"id": "ws_01j...",
"org_id": "org_01j...",
"name": "Acme Corp",
"cash_on_hand_cents": 5000000,
"created_at": "2026-03-01T00:00:00+00:00"
}
Updates workspace name and/or cash on hand. Only fields included in the request body are changed. Supports idempotency.
{
"name": "Acme Corp", // optional
"cash_on_hand_cents": 7500000 // optional; pass null to clear
}
// Updated workspace object
Permanently deletes the workspace and all associated transactions, forecasts, scenarios, and alerts.
Returns 204 No Content.
Push expense or revenue events into a workspace. All ingest endpoints are idempotent
by default (use external_id on each line item to deduplicate across calls).
All amounts are in cents and must be positive integers for expenses
and revenue; refunds are stored internally as negative.
Ingests one or more expense events. The subtype field determines how expenses
affect runway: recurring_expense items factor into
recurring_burn_cents_30d and runway; one_time_expense items factor
into burn_cents_30d only.
{
"expenses": [
{
"subtype": "recurring_expense", // "recurring_expense" | "one_time_expense"
"amount_cents": 120000,
"currency": "USD", // default "USD"
"occurred_at": "2026-02-01",
"vendor": "AWS", // optional
"external_id": "aws-feb-2026" // optional, used for dedup
}
]
}
{
"ingest_id": "ing_01j...",
"status": "completed",
"inserted": 1,
"duplicates": 0
}
Ingests one or more manual revenue events. Use subtype: "recurring" for
subscription or retainer revenue — it will contribute to mrr_cents.
Use subtype: "one_time" for non-recurring income.
{
"revenue": [
{
"subtype": "recurring", // "recurring" | "one_time"
"amount_cents": 250000,
"currency": "USD",
"occurred_at": "2026-02-01",
"description": "Feb subscription", // optional
"external_id": "sub-feb-2026" // optional, used for dedup
}
]
}
{
"ingest_id": "ing_01j...",
"status": "completed",
"inserted": 1,
"duplicates": 0
}
Pulls all paid invoices, standalone charges, succeeded refunds, and subscription metadata directly from the Stripe API using your secret key. Pagination is handled automatically. Re-running the call is safe — previously ingested objects are skipped.
{
"stripe_api_key": "sk_live_..."
}
{
"ingest_id": "ing_01j...",
"status": "completed",
"inserted": 147,
"duplicates": 12
}
| Error code | Meaning |
|---|---|
| invalid_stripe_api_key | 422 — Stripe rejected the key as invalid |
| stripe_unavailable | 502 — Stripe API is unreachable |
Returns computed CFO metrics for a rolling 30-day window ending on as_of
(defaults to today). All calculations are derived deterministically from ingested transactions.
as_of=2026-02-28 // YYYY-MM-DD, defaults to today
{
"as_of": "2026-02-28",
"currency": "USD",
"gross_revenue_cents_30d": 2400000,
"refunds_cents_30d": -80000,
"net_revenue_cents_30d": 2320000,
"mrr_cents": 2150000,
"arr_cents": 25800000,
"burn_cents_30d": 480000,
"recurring_burn_cents_30d": 350000,
"one_time_expenses_cents_30d": 130000,
"cash_on_hand_cents": 5000000,
"runway_months": 14.29,
"warnings": []
}
warnings may contain { "code": "missing_cash", "message": "..." }
when cash_on_hand_cents is not set on the workspace (runway will be null).
A forecast projects MRR, cash, and runway month-by-month from as_of
for horizon_months months. Multiple variants (base/best/worst) can be
computed in a single call. starting_cash_cents is automatically
read from the workspace — set it with PATCH /v1/workspaces/{id} first.
Creates and saves a new forecast. Supports idempotency.
{
"name": "Q2 2026 Base Plan", // optional label
"as_of": "2026-02-28", // defaults to today
"horizon_months": 12, // required
"assumptions": {
"mrr_growth_pct": 6, // monthly MRR growth %
"monthly_logo_churn_pct": 2, // monthly logo churn %
"gross_margin_pct": 85 // gross margin %
},
"variants": {
"base": {},
"best": { "mrr_growth_pct": 9, "monthly_logo_churn_pct": 1 },
"worst": { "mrr_growth_pct": 2, "monthly_logo_churn_pct": 4 }
}
}
{
"id": "fc_01j...",
"workspace_id": "ws_01j...",
"org_id": "org_01j...",
"name": "Q2 2026 Base Plan",
"as_of": "2026-02-28",
"horizon_months": 12,
"assumptions": { /* merged assumptions */ },
"series": {
"months": [ "2026-03", "2026-04", /* ... */ ],
"base": {
"mrr_cents": [ 2279000, 2415740, /* ... */ ],
"cash_cents": [ 4700000, 4400000, /* ... */ ],
"runway_months": [ 13.43, 12.57, /* ... */ ]
},
"best": { /* same shape */ },
"worst": { /* same shape */ }
},
"created_at": "2026-03-01T00:00:00+00:00"
}
Returns a lightweight list of forecasts (no series or assumptions). Paginated. Use GET by ID to retrieve full data.
{
"data": [
{
"id": "fc_01j...",
"name": "Q2 2026 Base Plan",
"as_of": "2026-02-28",
"horizon_months": 12,
"created_at": "2026-03-01T00:00:00+00:00"
}
],
"has_more": false
}
Returns the full forecast including assumptions and all variant series.
// Full forecast object (same shape as the create 201 response)
A scenario re-runs a saved forecast with a modified assumption (delta) and stores the resulting series alongside an impact summary. Use scenarios to answer what-if questions without overwriting your baseline.
Creates a what-if scenario against a baseline forecast. Supports idempotency.
{
"name": "10% price increase", // optional
"baseline_forecast_id": "fc_01j...", // required
"delta": {
"type": "price_change",
"pct": 0.10
}
}
// Other delta examples:
{ "type": "churn_change", "delta_pp": 1.5 } // +1.5pp churn
{ "type": "hire", "monthly_cost_cents": 1200000,
"start_month": "2026-04" } // new hire cost
{
"id": "sc_01j...",
"workspace_id": "ws_01j...",
"org_id": "org_01j...",
"name": "10% price increase",
"baseline_forecast_id": "fc_01j...",
"delta": { "type": "price_change", "pct": 0.10 },
"impact": {
"final_cash_delta_cents": 980000,
"final_mrr_cents": 3150000
},
"series": {
"months": [ /* ... */ ],
"scenario": {
"mrr_cents": [ /* ... */ ],
"cash_cents": [ /* ... */ ],
"runway_months": [ /* ... */ ]
}
},
"created_at": "2026-03-01T00:00:00+00:00"
}
Returns a lightweight list of scenarios (includes delta and impact, excludes series). Paginated. Use GET by ID to retrieve full series data.
{
"data": [
{
"id": "sc_01j...",
"name": "10% price increase",
"baseline_forecast_id": "fc_01j...",
"delta": { "type": "price_change", "pct": 0.10 },
"impact": {
"final_cash_delta_cents": 980000,
"final_mrr_cents": 3150000
},
"created_at": "2026-03-01T00:00:00+00:00"
}
],
"has_more": false
}
Returns the full scenario including the projected series.
// Full scenario object (same shape as the create 201 response)
Evaluates alert rules against current metrics and returns any active alerts. Results are computed fresh on each call and persisted, replacing any previously stored alerts.
as_of=2026-02-28 // YYYY-MM-DD, defaults to today
{
"data": [
{
"id": "al_01j...",
"type": "runway_low",
"severity": "critical",
"message": "Runway is below 6 months (2.5).",
"payload": { "runway_months": 2.5 },
"workspace_id": "ws_01j...",
"org_id": "org_01j...",
"created_at": "2026-03-01T00:00:00+00:00"
}
]
}
| Alert type | Severity | Fires when |
|---|---|---|
| runway_low | warn / critical | Runway < 6 months (critical if < 3 months) |
| mrr_decline | warn | MRR fell > 8% vs prior 30 days — only fires when the workspace has recurring revenue |
| revenue_decline | warn | Net revenue fell > 8% vs prior 30 days |
| refund_spike | warn | Refund volume is more than 2× the previous 30-day period |
Generates an AI-powered advisory memo grounded entirely in computed metrics and alerts.
Requires HUGGINGFACE_API_KEY to be configured. If the AI provider is
unavailable, the endpoint returns a fallback response with the raw metrics and alerts
so callers can still display data.
as_of=2026-02-28 // YYYY-MM-DD, defaults to today
{
"workspace_id": "ws_01j...",
"as_of": "2026-02-28",
"summary": "Cash position is stable with 14.3 months runway...",
"suggestions": [ "Consider increasing the marketing budget..." ],
"risks": [ "Refund volume rose 2.3x last month." ],
"grounding": {
"workspace_name": "Acme Corp",
"alert_count": 1,
"alerts": [ /* alert objects */ ],
"metrics": { /* metrics object */ }
},
"provider": "huggingface",
"model": "mistralai/Mistral-7B-Instruct-v0.2",
"ai_available": true
}
{
"workspace_id": "ws_01j...",
"as_of": "2026-02-28",
"summary": "AI is currently unavailable. Returning computed metrics and alerts only.",
"suggestions": [],
"risks": [],
"grounding": { /* metrics + alerts */ },
"provider": "fallback",
"model": null,
"ai_available": false,
"ai_error": {
"type": "configuration_error",
"code": "huggingface_not_configured",
"message": "HUGGINGFACE_API_KEY is not set."
}
}
Register, set up a workspace, ingest data, and pull metrics in one shell session.
# 1. Register
TOKEN=$(curl -s -X POST https://mycfo.vercel.app/v1/auth/register \
-H "Content-Type: application/json" \
-d '{"org_name":"Acme","email":"ceo@acme.com","password":"hunter2"}' \
| python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
# 2. Create workspace
WS=$(curl -s -X POST https://mycfo.vercel.app/v1/workspaces \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"Acme Corp","cash_on_hand_cents":5000000}' \
| python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
# 3. Ingest expenses
curl -s -X POST https://mycfo.vercel.app/v1/workspaces/$WS/ingest/expenses \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"expenses":[
{"subtype":"recurring_expense","amount_cents":120000,"occurred_at":"2026-02-01","vendor":"AWS"},
{"subtype":"one_time_expense","amount_cents":500000,"occurred_at":"2026-02-15","vendor":"Design contractor"}
]}'
# 4. Ingest recurring revenue
curl -s -X POST https://mycfo.vercel.app/v1/workspaces/$WS/ingest/revenue \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"revenue":[
{"subtype":"recurring","amount_cents":250000,"occurred_at":"2026-02-01","description":"Feb subscriptions"}
]}'
# 5. Pull metrics
curl -s "https://mycfo.vercel.app/v1/workspaces/$WS/metrics?as_of=2026-02-28" \
-H "Authorization: Bearer $TOKEN"
# 6. Run a 12-month forecast
FC=$(curl -s -X POST https://mycfo.vercel.app/v1/workspaces/$WS/forecasts \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"Base plan","horizon_months":12,
"assumptions":{"mrr_growth_pct":6,"monthly_logo_churn_pct":2,"gross_margin_pct":85},
"variants":{"base":{},"best":{"mrr_growth_pct":9},"worst":{"mrr_growth_pct":2}}}' \
| python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
# 7. What-if: 10% price increase
curl -s -X POST https://mycfo.vercel.app/v1/workspaces/$WS/scenarios \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"name\":\"Price +10%\",\"baseline_forecast_id\":\"$FC\",\"delta\":{\"type\":\"price_change\",\"pct\":0.10}}"
# 8. Check alerts
curl -s "https://mycfo.vercel.app/v1/workspaces/$WS/alerts" \
-H "Authorization: Bearer $TOKEN"