API & SDK map
Builders consume published SDKs and documented service surfaces — never private internal packages from another team, and never a marketing route treated as an operational API. This page is the map of what "published" means on this platform and how the surfaces fit together.
The contract-first rule
Every public surface is defined by its contract before it is implemented: HTTP APIs are specified in OpenAPI, and service-to-service interfaces in protocol-buffer definitions. Those specifications are the source of truth — SDKs are generated from them, documentation is derived from them, and any drift between a specification and its implementation is treated as a build failure, not a documentation bug. API versioning & compatibility describes how those contracts evolve without breaking consumers.
The surfaces
- Identity & session. The Triangle token exchange: a session from the sign-on layer is exchanged for a platform access token carrying role, tier, and brand context. See Triangle sessions & tokens.
- Brand-scoped data access. Data reads and writes flow through published SDKs that attach brand context to every call — the same database-level isolation the console gets, with no privileged side path.
- Events & webhooks. Real-time integration happens over the event substrate: signed webhooks out, validated events in. See webhooks & the event substrate.
- Ledger & metering. Royalty and fee records are written by the platform through the metering ledger; ventures read outcomes through published surfaces, never by writing ledger rows directly. See royalty metering & the ledger.
What "published" guarantees
- Documented contracts with machine-readable error codes in stable namespaces — an integration can branch on error codes, not on message text.
- Additive change only. Existing fields and endpoints do not change meaning underneath a consumer; new behavior arrives as new surface. The full policy is at API versioning & compatibility.
- Explicit isolation. Every call carries brand context and authenticated identity; there is no anonymous operational surface. The checklist for integrating safely is at integration boundaries.
Endpoint reference
The console's backend-for-frontend surface lives under /api on the console deployment. Every endpoint speaks JSON in and out, and responses are marked no-store. The reference below documents the public intake surface and the staff surfaces an integrator meets first: billing, treasury, and metering.
Conventions
- Two auth models. The public intake POST expects a deployment-issued shared key in the
x-intake-keyheader, compared timing-safe. Everything else requires a Triangle staff session — cookie orauthorization: Bearertoken — carrying an operator tier; a request that presents a bearer token additionally passes an RBAC check (readfor GET,managefor mutations). - Error envelope. Failures return
{ "error": string, "reason"?: string }: 400 for an invalid body or filter value, 401 for missing or invalid authentication, 403 for insufficient tier or a policy denial, 404 for an unknown id, 409 for a state conflict, 500 for an internal error. - Honest freshness. Staff list surfaces currently serve from per-process stores seeded with fixture data and say so in the payload (
source: "memory"); treasury invoices read from the live kernel (source: "kernel"). Amounts are integer minor units of the record's currency.
Intake — public submission
POST /api/intake — public (shared key, no session). Creates one inquiry in the staff inbox.
Request body:
{
"desk": "operations | partnerships | capital | trust | careers | general",
"name": "string (required)",
"email": "string (required)",
"message": "string (required)",
"role": "string (optional)",
"company": "string (optional)",
"fields": { "<label>": "string" }
}Success 201:
{ "id": "inq_…", "status": "new" }Errors: 400 for invalid JSON or a missing required field, 401 for a missing or mismatched key.
curl -X POST 'https://<console-host>/api/intake' -H 'content-type: application/json' -H 'x-intake-key: <deployment-intake-key>' -d '{"desk":"operations","name":"Ada Osei","email":"ada@example.com","message":"Pilot inquiry for a 12-unit fitness group."}'Intake — staff inbox
GET /api/intake — staff. Lists inquiries, optionally narrowed by ?desk= and ?status= (new | in-progress | resolved; any other filter value is a 400).
Response 200:
{
"inquiries": [
{
"id": "inq_…",
"desk": "operations",
"name": "string",
"email": "string",
"message": "string",
"role": "string | null",
"company": "string | null",
"fields": { "<label>": "string" },
"status": "new | in-progress | resolved",
"owner": "string | null",
"intake": "key | dev",
"createdAt": "ISO 8601 timestamp"
}
],
"summary": { "total": 0, "fresh": 0, "inProgress": 0, "resolved": 0, "byDesk": { "operations": 0 } },
"source": "memory"
}PATCH /api/intake — staff (mutation). Mutates one inquiry and returns the refreshed inbox payload. Status transitions are forward-only (new → in-progress → resolved); an invalid transition is a 409, an unknown id a 404.
{ "id": "inq_…", "action": "assign | status", "owner": "string (required with assign)", "status": "in-progress | resolved (required with status)" }curl -X PATCH 'https://<console-host>/api/intake' -H 'authorization: Bearer <staff-access-token>' -H 'content-type: application/json' -d '{"id":"inq_…","action":"status","status":"in-progress"}'Billing — recurring schedules
GET /api/billing/recurring — staff. Lists recurring invoice schedules; ?tenant=<tenant-path> narrows to one tenant, omitted returns the cross-venture set.
Response 200:
{
"schedules": [
{
"id": "string",
"tenant_path": "string",
"name": "string",
"status": "active | paused | completed | cancelled",
"interval": "weekly | biweekly | monthly | quarterly | semiannually | yearly",
"day_of_month": 1,
"start_at": "ISO 8601",
"end_at": "ISO 8601 | null",
"next_run_at": "ISO 8601",
"auto_issue": true,
"template": {
"lines": [{ "description": "string", "quantity": 1, "unit_amount_minor": 0 }],
"currency": "usd",
"payment_terms_days": 30,
"memo": "string (optional)",
"counterparty_ref": "string (optional)",
"counterparty_name": "string (optional)"
},
"generated_count": 0,
"generated_total_minor": 0,
"last_generated_at": "ISO 8601 | null",
"created_at": "ISO 8601",
"updated_at": "ISO 8601"
}
],
"summary": { "active": 0, "paused": 0, "monthlyEquivalentMinor": 0, "nextRunAt": "ISO 8601 | null" },
"source": "memory"
}POST /api/billing/recurring — staff (mutation). Lifecycle action on one schedule; returns the refreshed payload.
{ "action": "pause | resume | cancel", "scheduleId": "string" }Errors: 400 for a malformed action, 404 for an unknown schedule, 409 for an illegal transition (pausing a paused schedule, cancelling a completed one).
curl -X POST 'https://<console-host>/api/billing/recurring' -H 'authorization: Bearer <staff-access-token>' -H 'content-type: application/json' -d '{"action":"pause","scheduleId":"rsch_…"}'Billing — credit notes
GET /api/billing/credit-notes — staff. Lists credit notes; ?tenant=<tenant-path> narrows as above.
Response 200:
{
"notes": [
{
"id": "string",
"tenant_path": "string",
"credit_number": "string",
"invoice_id": "string",
"amount_minor": 0,
"applied_minor": 0,
"reason": "string",
"notes": "string | null",
"status": "draft | issued | applied | void",
"issued_at": "ISO 8601 | null",
"voided_at": "ISO 8601 | null",
"created_at": "ISO 8601",
"updated_at": "ISO 8601"
}
],
"summary": { "open": 0, "remainingMinor": 0, "appliedMinor": 0, "gated": 0 },
"source": "memory"
}POST /api/billing/credit-notes — staff (mutation). Lifecycle action on one note; returns the refreshed payload.
{
"action": "issue | apply | void",
"noteId": "string",
"invoiceTotalMinor": 0,
"invoiceOutstandingMinor": 0,
"amountMinor": 0
}issue requires invoiceTotalMinor; apply requires invoiceOutstandingMinor and accepts amountMinor for a partial application; void needs neither. Errors: 400 for a missing required amount, 404 for an unknown note, 409 for an illegal transition (issuing a non-draft, applying beyond the remaining credit or the invoice's outstanding balance, voiding a partially applied note).
Treasury — invoices
GET /api/treasury/invoices — staff, senior operator tier (the set is cross-venture). No parameters. This surface reads from the live kernel, not a memory store.
Response 200:
{
"dues": [
{
"id": "string",
"line": "<Venture> — <Mon YYYY>",
"rate": "USD-formatted string",
"coverage": "<n> ledger rows",
"current": 60
}
],
"count": 1,
"source": "kernel"
}current is the collection-progress signal: 100 for paid, 60 for issued, 0 for draft or void.
curl 'https://<console-host>/api/treasury/invoices' -H 'authorization: Bearer <staff-access-token>'Metering — usage events
GET /api/metering/events — staff. The most recent usage events observed by this deployment: a process-local ring buffer of the last 200 records, newest first. An operational view, not a durable log.
Response 200:
{
"entries": [
{
"receiptId": "string",
"capability": "billing.recurring.pause",
"quantity": 1,
"ventureId": "string",
"tenantId": "string | null",
"actorId": "string | null",
"recordedAt": "ISO 8601"
}
],
"transport": "string — the active metering transport",
"source": "memory"
}transport names which metering transport the deployment is running: the event substrate when configured, an in-memory publisher in development, noop when neither is present.
Reading the rest of the developer section
The developer docs are written to be read in a short arc:
- This page — what is published, what "published" guarantees, and the endpoint shapes to build against.
- API versioning & compatibility — how contracts evolve without breaking you: additive change, drift as a build failure, and the deprecation practice.
- Webhooks & the event substrate — real-time integration: signed deliveries, at-least-once semantics, and how to verify and deduplicate correctly.
- Integration boundaries — the seven-point checklist that decides whether an integration is actually done.
Behind those sit the identity and data foundations the APIs enforce: Triangle sessions & tokens for how credentials and claims work, and database isolation mechanics for the tenant boundary every call runs inside.
Note
Endpoint shapes on this page are public by design — method, path, fields, and the error envelope tell an integrator nothing sensitive. Credentials, environment URLs, and live data stay behind authenticated developer access: request access through the standard path and state the partner/integrator role.
