Developer Documentation
Integrate Carbon Zero with your systems — a REST API for every ESG reporting operation, API-key authentication with least-privilege grants, and signed webhooks for real-time events. This guide is written for developers building on a tenant workspace: ingesting metric submissions, streaming vessel fuel telemetry, computing Scope 1–3 emissions, and publishing disclosures.
1. Overview & architecture
Carbon Zero is a multi-tenant ESG reporting & compliance platform. It
exposes a JSON REST API under /api/v1. Requests are authenticated with either a user
JWT (from login) or a workspace API key, and authorized against
the caller's role/grant permissions. Real-time notifications — entity, submission, disclosure,
score and ticket events — are delivered out-of-band via signed webhooks.
/api/v1/*] N --> DB[(Workspace data)] N -. emits .-> Q[Webhook outbox] Q -->|signed POST| R[Your webhook receiver]
/api/docs (ReDoc at
/api/redoc, raw schema at /api/openapi.json) to browse every endpoint,
model, and field interactively — also linked from the API Explorer
tab. In production it is access-restricted (HTTP Basic auth) — ask your Carbon
Zero administrator for credentials.2. Base URL & conventions
| Base URL | https://<your-workspace-domain>/api/v1 |
| Content type | application/json (file uploads use multipart/form-data) |
| Auth | X-API-Key: cz_live_… or Authorization: Bearer <token> |
| Errors | Non-2xx with a JSON body { "detail": "message" } |
| Emissions | Reported in tCO2e with a WTT / TTW split (Well-to-Tank + Tank-to-Wake → total) |
| Money | JSON numbers, per-currency (workspace default AED) — never summed across currencies |
| Dates | Dates YYYY-MM-DD; timestamps ISO-8601 UTC; ids are UUID strings |
Common status codes: 200/201 success, 400 validation/business error,
401 missing/invalid credentials, 403 insufficient permission,
404 not found, 422 request schema validation. New workspaces default to
AED currency, Asia/Dubai timezone and dd/mm/yyyy
date format (editable in Settings).
3. Authentication
User JWT
Logging in returns a bearer token for that user, scoped to their workspace and role.
# Login → JWT
curl -X POST https://<domain>/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"you@org.com","password":"••••••"}'
# → { "access_token": "eyJ…", "token_type": "bearer", "user": {…}, "tenant": {…} }
curl https://<domain>/api/v1/auth/me -H "Authorization: Bearer eyJ…"
A user can update their own profile with PATCH /api/v1/auth/me/preferences
(e.g. {"date_format":"mm/dd/yyyy"}) and change their password via
POST /api/v1/auth/me/change-password; the workspace defaults (currency, timezone,
date_format) are set via PATCH /api/v1/tenants/settings.
The same login also sets the session as HTTP-only cookies (used by the Carbon
Zero web app, so the token is never exposed to JavaScript) — an httpOnly cz_access
cookie, a JS-readable cz_csrf cookie, and a rotating cz_refresh. The body
access_token is retained for programmatic callers. Cookie-authenticated, state-changing
requests must echo the readable cz_csrf value in an X-CSRF-Token header
(double-submit CSRF); access cookies are renewed via POST /auth/refresh and cleared by
POST /auth/logout. A main.py middleware bridges the cookie →
Authorization: Bearer for every token-decode dependency.
For integrations, prefer an API key (below) — it needs no cookies, CSRF, or refresh
handling.
/auth/login returns
{ "mfa_required": true, "pending_token": "…", "methods": [...] } instead of a session.
Complete it with POST /auth/mfa/verify (the pending_token + the user's
code); email codes can be re-sent via POST /auth/mfa/email/resend. Enrol via
/auth/mfa/setup (TOTP) or /auth/mfa/setup-email, then
/auth/mfa/enable. API keys are not subject to MFA, which is another
reason to use them for server-to-server access.API keys (recommended for integrations)
An API key is a standalone principal — it authenticates as itself, not the user
who created it, so it survives that user leaving. Mint keys under Developer → API
Keys. When minting a key you pick a name, optional expiry, and exactly which
(resource, action) grants it should hold.
- The secret looks like
cz_live_…and is shown exactly once at creation — store it securely. Only a hash is kept server-side. - A key's grants are validated against the creator's own permissions at creation — a key can never exceed its creator (least privilege).
- Pass it as
X-API-Key: cz_live_…orAuthorization: Bearer cz_live_…(thecz_live_prefix disambiguates from a JWT). - Revocation is immediate;
expires_atis enforced; an API-key principal can call business endpoints but cannot mint other keys.
| GET | /api-keys | List the workspace's keys (api_keys:read) |
| POST | /api-keys | Mint a key — name, optional expiry, (resource, action) grants ≤ your own; the cz_live_… secret is returned once (api_keys:create) |
| DELETE | /api-keys/{id} | Revoke a key immediately (api_keys:revoke) |
# Same request, with an API key
curl https://<domain>/api/v1/reporting-entities \
-H "X-API-Key: cz_live_xxxxxxxxxxxxxxxx"
cz_live_ API-key path is the intended mechanism for shore-side machine
ingestion of vessel fuel feeds.The RBAC model
Every gated endpoint requires a (resource, action) permission. has_permission
is exact-match (no wildcards). Roles grant permissions; API keys carry a subset of
grants. The owner role's grants are seeded at tenant creation. Resources:
| Resource | Typical actions | Guards |
|---|---|---|
reporting_entities | read, create, update, archive, view_all, assign | Org / subsidiary / facility / supplier / vessel records & hierarchy; owner+branch scoping |
frameworks | read, manage, publish | Frameworks (GRI/SASB/CSRD-ESRS/TCFD/GHG-Protocol) & lifecycle |
metrics | read, manage | The metric / data-point catalogue per framework |
periods | read, manage | Reporting periods / campaigns (open/closed) |
submissions | read, create, update, verify | The metric-submission ledger; also gates the emissions calculator & fuel feeds |
disclosures | read, create, update, approve, publish | ESG reports & their immutable published snapshot |
scoring | read, manage, run | Scoring models, criteria & computed scores |
targets | read, manage | Per-metric goals & progress |
reports | read, build | Report builder catalogue, runs & saved definitions |
processes | read, create, update, publish, execute | Event-driven automation rules & runs |
customer_care | access | Entity-360 search & overview (ignores owner/branch scoping) |
tickets | read, create, update, assign, manage | Support tickets & comment threads |
tenant_settings | read, manage | Workspace settings (currency, timezone, branding, flags) |
branches | read, create, update, manage, view_all | Branch records & membership |
roles / users | read, manage / read, create, update, suspend, impersonate | Roles & member management |
reference_data | read, manage | Master-data lists (frameworks, units, sectors, countries) |
custom_fields | read, manage | Custom-field definitions & values per object type |
webhooks | read, create, update, delete | Outbound webhook endpoints & deliveries |
api_keys | read, create, revoke | API-key management |
is_platform_admin flag via
require_platform_admin — not by per-tenant RBAC.4. Quickstart
Mint a key with submissions grants in the dashboard, then create a reporting entity
and upsert a metric submission:
# 1. Create a reporting entity (organization / subsidiary / facility / supplier / vessel)
curl -X POST https://<domain>/api/v1/reporting-entities \
-H "X-API-Key: cz_live_…" -H "Content-Type: application/json" \
-d '{"name":"MV Gulf Pioneer","entity_type":"vessel","identifier":"9876543"}'
# 2. Upsert a metric submission (unique per period × entity × metric)
curl -X POST https://<domain>/api/v1/submissions \
-H "X-API-Key: cz_live_…" -H "Content-Type: application/json" \
-d '{"entity_id":"<entity-uuid>","period_id":"<period-uuid>","metric_id":"<metric-uuid>","value":"1250.5"}'
5. API reference
All paths are relative to /api/v1. Browse full request/response schemas in the live
/api/docs explorer (access-restricted) or the API
Explorer tab. Highlights by domain:
Reporting entities
A reporting entity is the subject of ESG data — an
organization, subsidiary, facility, supplier or
vessel — with a parent_id hierarchy and custom-field values. Entities are
owner + branch scoped (a caller sees only their accessible_entity_ids
unless they hold reporting_entities:view_all). Vessels carry their IMO
number as identifier and are the subject of the fuel-feed Scope 1 evidence layer.
| GET | /reporting-entities | List entities — filters by type / parent / search (reporting_entities:read) |
| POST | /reporting-entities | Create an entity (entity_type, optional parent_id, identifier, custom fields) — emits entity.created |
| GET | /reporting-entities/{id} | Get one entity |
| PATCH | /reporting-entities/{id} | Update / archive an entity — emits entity.updated / entity.archived |
Frameworks & metric catalogue
A framework (GRI, SASB, CSRD-ESRS, TCFD, GHG-Protocol, plus the seeded UAE &
maritime standards) defines a catalogue of metrics — each with a
pillar (E/S/G), a value_type and a unit. Frameworks move
draft → active; metrics belong to a framework.
| GET | /frameworks | List frameworks (frameworks:read) |
| POST | /frameworks | Create a framework (frameworks:manage) |
| PATCH | /frameworks/{id} | Update / publish (draft → active) a framework (frameworks:manage / publish) |
| GET | /frameworks/{id}/metrics | List a framework's metrics (metrics:read) |
| POST | /frameworks/{id}/metrics | Add a metric — pillar, value_type, unit (metrics:manage) |
| PATCH DELETE | /frameworks/metrics/{metric_id} | Edit / delete a metric |
Reporting periods
A reporting period (campaign, e.g. FY2025) is a collection cycle. Submissions and
scores are keyed by period. A period is open or closed.
| GET | /periods | List periods (periods:read) |
| POST | /periods | Open a period — emits period.opened (periods:manage) |
| PATCH | /periods/{id} | Update / close a period — emits period.closed |
Metric submissions
The metric-submission ledger is the heart of data collection: one value per
(period × entity × metric), with evidence and a status. POST is an
upsert — there is a unique triple, so re-posting the same combination updates in
place rather than creating a duplicate. The lifecycle is
draft → submitted → verified → rejected, advanced with the review endpoint.
| GET | /submissions | List submissions — filter by period / entity / metric / status (submissions:read) |
| POST | /submissions | Upsert a submission for a (period, entity, metric) triple — emits submission.created / submission.submitted (submissions:create / update) |
| POST | /submissions/{id}/review | Verify or reject a submission — emits submission.verified (submissions:verify) |
Disclosures
A disclosure (ESG report) is produced per (entity × framework ×
period) and moves draft → in_review → approved → published. On
publish the disclosure takes an immutable snapshot of its metric
values, plus a transition history — so a published report never drifts as later submissions change.
| GET | /disclosures | List disclosures (disclosures:read) |
| POST | /disclosures | Create a draft disclosure — emits disclosure.created (disclosures:create) |
| GET | /disclosures/{id} | Get one disclosure (with snapshot & history) |
| POST | /disclosures/{id}/transition | Advance the lifecycle (submit / approve / publish / send back) — emits disclosure.submitted / .approved / .published / .sent_back (disclosures:update / approve / publish) |
ESG scoring
A scoring model holds weighted criteria. Each criterion is
either a numeric set of bands or a simpleeval
expression over a metrics context. The weighted average of criterion scores yields a
0–100 score, mapped to a tier from the model's tiers. An
EsgScore is upserted per (model × entity × period) with a
per-criterion breakdown.
| GET | /scoring/models | List scoring models (scoring:read) |
| POST | /scoring/models | Create a model (weights, tiers) (scoring:manage) |
| PATCH | /scoring/models/{id} | Update a model |
| POST | /scoring/models/{id}/criteria | Add a criterion (numeric bands or a simpleeval expression + weight) |
| DELETE | /scoring/criteria/{id} | Delete a criterion |
| GET | /scoring/scores | List computed scores — filter by model / entity / period (scoring:read) |
| POST | /scoring/run | Run a model for an entity × period → upserts an EsgScore — emits score.computed / score.below_threshold (scoring:run) |
Targets
A target sets a per-(entity × metric) baseline → target
goal. Progress is direction-aware (reduction vs increase) and computed live as a
progress_pct from the latest submission for that metric.
| GET | /targets | List targets with live progress (targets:read) |
| POST | /targets | Create a target (entity, metric, baseline, target, direction) (targets:manage) |
| PATCH DELETE | /targets/{id} | Update / delete a target — may emit target.breached |
Emissions (Scope 1–3 calculator)
The emissions module computes activity-based (primary) and
spend-based (fallback) emissions, each split into WTT + TTW → total
tCO2e. An EmissionFactor catalogue (kgCO2e/unit, WTT+TTW) is
lazy-seeded on first use with maritime fuels
(HFO / VLSFO / MGO / LNG / methanol / biofuel / ammonia, per tonne), transport modes (per
tonne-km), and spend categories (per currency). An ActivityRecord computes from a fuel
burn / tonne-km amount (activity-based) or a spend amount (spend-based); the summary rolls results up
by Scope 3 category + method. Gated on the submissions resource (no
new RBAC).
| GET | /emissions/factors | The emission-factor catalogue (WTT + TTW per unit); lazy-seeds on first use |
| GET | /emissions/activities | List activity records (computed wtt / ttw / total tCO2e) |
| POST | /emissions/activities | Add an activity record — method → factor → inputs (fuel burn / tonne-km / spend) → computed emissions |
| DELETE | /emissions/activities/{id} | Delete an activity record |
| GET | /emissions/summary | Roll-up by Scope 3 category + method (activity-based vs spend-based) |
Vessel fuel feeds (Scope 1 evidence)
For maritime Scope 1, accuracy lives in the fuel-mass figure (the IMO emission factor is fixed), so this layer ingests granular fuel observations per vessel and builds an auditable, reconciled trail before it ever reaches the emissions summary.
Each FuelConsumptionRecord carries a source that maps to an IMO
DCS / EU-MRV method and a data-quality tag:
| Source | Method | Data quality |
|---|---|---|
bdn (Bunker Delivery Note) | A | primary |
tank_sounding | B | primary |
mfm (mass flow meter) | C | primary |
ams_feed (automated monitoring) | C | primary |
noon_report | — (no auditable method) | estimated |
manual | — | estimated |
Ingest is idempotent on the unique tuple
tenant + entity + source + source_ref — re-posting the same observation updates it in
place, so at-least-once / retried delivery from shore-side systems is safe. Each record computes its
TTW (Scope 1) and WTT (Scope 3 cat 3) via the shared
emissions_service. The Bunker Delivery Note is the auditable
"fuel uplifted" anchor; reconcile compares Σ feed vs Σ BDN per
(vessel × period), computes per-fuel variance %, flags anything beyond the 5% tolerance,
and marks reconciled records. rollup then collapses the reconciled feed into one
emissions.ActivityRecord per (vessel × period × fuel) (idempotent —
refreshes in place), so the existing summary / scoring / disclosure pipeline consumes it unchanged.
Gated on the submissions resource; the cz_live_ API-key path is the
intended channel for machine ingestion.
| POST | /emissions/fuel-feed | Idempotent batch ingest of fuel observations (unique tenant+entity+source+source_ref); computes TTW + WTT |
| GET | /emissions/fuel-feed/records | List ingested fuel observations (with method + data-quality tags) |
| POST | /emissions/fuel-feed/bdn | Record a Bunker Delivery Note (the auditable "fuel uplifted" anchor) |
| GET | /emissions/fuel-feed/bdn | List Bunker Delivery Notes |
| POST | /emissions/fuel-feed/reconcile | Σ feed vs Σ BDN per vessel × period (5% tolerance) → per-fuel variance, flags, marks reconciled |
| POST | /emissions/fuel-feed/rollup | Collapse reconciled feed → one ActivityRecord per vessel × period × fuel (idempotent) |
# Shore-side machine ingest of a noon report (idempotent on source_ref)
curl -X POST https://<domain>/api/v1/emissions/fuel-feed \
-H "X-API-Key: cz_live_…" -H "Content-Type: application/json" \
-d '{"records":[{"entity_id":"<vessel-uuid>","source":"noon_report","source_ref":"NR-2025-04-12","fuel_type":"VLSFO","quantity_tonnes":42.7,"period_id":"<period-uuid>"}]}'
Customer care (entity-360) & support tickets
The customer-care module is an entity-360 view — search any entity and pull a
consolidated overview (submissions, disclosures, scores, targets, tickets, notes). It is gated on
customer_care:access and deliberately ignores owner/branch scoping.
Support tickets are entity-linked, numbered TKT-{year}-{n}, with a
comment thread and status / priority / assignee.
| GET | /customer-care/search | Search any entity (ignores scoping) (customer_care:access) |
| GET | /customer-care/entities/{id}/overview | The 360 overview for an entity |
| GET | /customer-care/entities/{id}/{tab} | One tab's data (submissions / disclosures / scores / targets / tickets / notes) |
| POST | /customer-care/entities/{id}/notes | Add a care note to an entity |
| GET | /tickets | List tickets (tickets:read) |
| POST | /tickets | Open a ticket — emits ticket.created (tickets:create) |
| GET | /tickets/{id} | Get a ticket with its thread |
| PATCH | /tickets/{id} | Update status / priority / assignee — emits ticket.assigned / ticket.resolved (tickets:update / assign) |
| POST | /tickets/{id}/comments | Add a comment — emits ticket.commented |
Report builder & automation
The report builder is a compose-over-catalogue engine: pick a
source (entities / submissions / disclosures / scores / targets), choose whitelisted
fields, run, and optionally save the definition or export CSV. Automation is an
event-driven rules engine — a rule's trigger is a webhook event, with an optional
simpleeval condition over the payload and an action of
create_ticket | add_note | emit_event; runs are logged as a ProcessRun and
fired alongside the webhook emit.
| GET | /report-builder/catalogue | The available sources + whitelisted fields (reports:read) |
| POST | /report-builder/run | Run a report (source + fields + filters) (reports:build) |
| POST | /report-builder/export | Export the result as CSV |
| GET POST | /report-builder/definitions | List / save report definitions |
| GET | /automation/event-types | The webhook events a rule may trigger on (processes:read) |
| GET POST | /automation/rules | List / create rules (trigger event, condition, action) (processes:create) |
| PATCH DELETE | /automation/rules/{id} | Update / delete a rule (processes:update) |
| GET | /automation/runs | The ProcessRun log of rule executions |
Dashboard, workspace & settings
The dashboard summary is branch∩owner-scoped analytics; the rest are workspace administration — settings, roles & members, invitations, branches, maker-checker dual control, reference data, custom fields, webhooks and billing.
| GET | /dashboard/summary | Entity counts by type, framework / period counts, submission completeness, disclosure pipeline, score avg + tier distribution, active targets (branch∩owner scoped) |
| GET PATCH | /tenants/settings | Workspace settings — currency, timezone, date_format, branding, flags (defaults AED + Asia/Dubai + dd/mm/yyyy) |
| POST | /tenants/register | Register a new workspace (public) |
| GET | /roles | List / POST create roles; PATCH / DELETE /roles/{id} (roles:read / manage); /roles/permissions lists the catalogue |
| GET | /roles/members/list | List members; /members/{id}/role (PATCH), /deactivate, /reactivate, /reset-password (users:*) |
| POST | /users/invite | Invite a member; /invitations (list), /invite/{id} (DELETE), /invite/accept |
| GET | /branches | List / create branches; /accessible, /{id}/members (branches:*) |
| GET PUT | /maker-checker/config | Per-module dual-control config; /requests to list, /requests/{id}/approve|reject|cancel (checker ≠ requester) |
| GET | /reference-data/lists | Master-data lists + items (frameworks / units / sectors / countries, incl. UAE & maritime) (reference_data:*) |
| GET | /webhooks | Register / list / update / delete endpoints; /event-types, /{id}/deliveries, /roll-secret, /test (webhooks:*) |
| GET | /billing | Plan + trial state; /billing/plans, POST /billing/select-plan (free / starter / growth / enterprise, 14-day trial). Public unauthenticated GET /api/v1/plans for the marketing pricing page |
reporting_entity) plus their values,
gated on custom_fields:read / manage. Entity create/update accepts
custom-field values inline.Platform admin (cross-tenant)
The platform admin console is a cross-tenant operator surface under
/admin, gated by the is_platform_admin flag via
require_platform_admin — separate from per-tenant RBAC. It exposes
cross-tenant stats, tenant management, owner impersonation, an editable plan catalogue and a
read-only SQL console.
| GET | /admin/stats | Cross-tenant platform statistics |
| GET | /admin/tenants | List all tenants; /tenants/{id} for detail |
| POST | /admin/tenants/{id}/plan | Set a tenant's plan; /trial to set trial state |
| POST | /admin/tenants/{id}/suspend | Suspend (→ billing canceled / locked; business endpoints 402) / /reactivate |
| POST | /admin/tenants/{id}/impersonate | Owner impersonation (return via /auth/impersonate/stop) |
| GET PUT | /admin/plans · /plan-config | The editable plan catalogue — PUT /admin/plans/{key} overrides, POST /admin/plans/{key}/reset reverts to default; overlaid onto live PLANS immediately |
| GET POST | /admin/sql · /sql/schema | Read-only SQL console — every query runs in a rolled-back READ ONLY txn with a statement_timeout + row cap |
6. Webhooks
Register HTTPS endpoints to be notified when events occur (e.g. a disclosure is published).
Configure them under Developer → Webhooks (or POST /webhooks): a URL,
the events to subscribe to, and a signing secret (whsec_…) shown
once.
Delivery flow
The request & envelope
Each delivery is an HTTP POST with these headers and a stable envelope:
| Header | Meaning |
|---|---|
X-CarbonZero-Event | The event type (also in body as event). |
X-CarbonZero-Delivery | Unique delivery / event id — your idempotency key. |
X-CarbonZero-Signature | HMAC-SHA256(raw_body, signing_secret), hex-encoded. |
{
"event": "disclosure.published",
"event_id": "3f12e818-55b8-440c-95ee-f6bdcf31b25e",
"occurred_at": "2026-06-06T03:57:16.011200+00:00",
"tenant_id": "2eef904a-af99-44a4-9a5e-30628b0eb2bc",
"data": { /* event-specific */ }
}
Verifying signatures
Always verify before trusting a payload.
Python (FastAPI)
import hmac, hashlib, json
def verify(raw_body: bytes, sig: str, secret: str) -> bool:
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig or "")
@app.post("/webhooks/carbonzero")
async def receive(request: Request):
raw = await request.body() # raw bytes, not request.json()
if not verify(raw, request.headers.get("X-CarbonZero-Signature"), SECRET):
raise HTTPException(401, "bad signature")
event = json.loads(raw)
# dedupe on event["event_id"], then process async, return 2xx fast
Node (Express)
const crypto = require('crypto')
// capture raw body: app.use(express.raw({ type: 'application/json' }))
function verify(rawBody, sigHeader, secret) {
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex')
const a = Buffer.from(expected), b = Buffer.from(sigHeader || '')
return a.length === b.length && crypto.timingSafeEqual(a, b)
}
app.post('/webhooks/carbonzero', (req, res) => {
if (!verify(req.body, req.get('X-CarbonZero-Signature'), SECRET))
return res.sendStatus(401)
const evt = JSON.parse(req.body.toString())
// dedupe on evt.event_id, ack fast (2xx), process async
res.sendStatus(200)
})
Delivery semantics
- Respond 2xx quickly. Non-2xx / timeout / connection error = failure.
- Retries with backoff, then dead (manually retryable via the dashboard or
/{id}/deliveries/{delivery_id}/retry). The delivery id is stable across retries. - At-least-once → be idempotent. Deduplicate on
event_id. - No ordering guarantee. Use
occurred_atif you need order. - HTTPS only, publicly reachable (private/internal addresses are rejected).
Event types
| Event | data |
|---|---|
entity.created | A reporting entity was created. |
entity.updated | A reporting entity's fields changed. |
entity.archived | A reporting entity was archived. |
period.opened | A reporting period / campaign was opened. |
period.closed | A reporting period was closed. |
submission.created | A metric submission was created (draft) for a period × entity × metric. |
submission.submitted | A submission was submitted for review. |
submission.verified | A submission was verified. |
submission.overdue | An expected submission passed its due date. |
disclosure.created | A draft disclosure was created. |
disclosure.submitted | A disclosure entered review. |
disclosure.approved | A disclosure was approved. |
disclosure.published | A disclosure was published with an immutable metric snapshot. |
disclosure.sent_back | A disclosure was returned for changes. |
score.computed | An ESG score was (re)computed — score, tier, per-criterion breakdown. |
score.below_threshold | A computed score fell below a configured threshold. |
target.breached | A target's progress crossed into breach. |
ticket.created | A support ticket was opened. |
ticket.assigned | A ticket was assigned to a member. |
ticket.resolved | A ticket was resolved. |
ticket.commented | A comment was added to a ticket thread. |
ticket.overdue | A ticket passed its SLA / due date. |
Webhook endpoints are managed under /webhooks —
GET /webhooks/event-types lists the catalogue above, CRUD on /webhooks
registers endpoints, POST /webhooks/{id}/roll-secret rotates the whsec_
secret, GET /webhooks/{id}/deliveries inspects history,
POST /webhooks/{id}/deliveries/{delivery_id}/retry replays a dead delivery, and
POST /webhooks/{id}/test fires a test event.
7. End-to-end integration example
A typical server-to-server ESG reporting flow, combining the API with a webhook receiver:
- Create the reporting entity —
POST /reporting-entities(org / subsidiary / facility / supplier / vessel). - Define the framework + metrics —
POST /frameworks, thenPOST /frameworks/{id}/metricsfor each data point; publish the framework toactive. - Open a period —
POST /periods(e.g. FY2025). - Upsert submissions —
POST /submissionsper(period, entity, metric); re-posting updates in place. - Verify —
POST /submissions/{id}/review; listen forsubmission.verified. - Run scoring —
POST /scoring/run→ anEsgScore(0–100 → tier); listen forscore.computed. - Publish a disclosure —
POST /disclosuresthenPOST /disclosures/{id}/transitionthrough topublished; act ondisclosure.published(the snapshot is now immutable).
Maritime: vessel fuel feed → reconcile → roll-up
For Scope 1 fuel evidence on vessels, a second short flow feeds the same emissions pipeline:
- Ingest fuel observations —
POST /emissions/fuel-feed(idempotent ontenant+entity+source+source_ref); each computes TTW + WTT. - Record BDNs —
POST /emissions/fuel-feed/bdnas the auditable "fuel uplifted" anchor. - Reconcile —
POST /emissions/fuel-feed/reconcilecompares Σ feed vs Σ BDN per vessel × period, flags variance beyond 5%. - Roll up —
POST /emissions/fuel-feed/rollupcollapses the reconciled feed into oneActivityRecordper vessel × period × fuel — it then appears inGET /emissions/summaryand flows into scoring & disclosures unchanged.
8. Reference appendix
Idempotency & safety
- Webhook deliveries are at-least-once; dedupe on
event_id. - Metric submissions are an upsert on the unique
(period × entity × metric)triple — a re-post updates in place, never duplicates. - Vessel fuel-feed ingest is idempotent on
tenant + entity + source + source_ref; the roll-up refreshes itsActivityRecordin place — safe for at-least-once delivery. - A disclosure's published snapshot is immutable — later submission changes never alter a published report.
- Treat webhook
dataas forward-compatible — ignore unknown fields rather than rejecting.
Versioning & limits
- The API is versioned in the path (
/api/v1). - Design for least-privilege keys and rotate manually by minting a new key and revoking the old.
- Emissions are reported in tCO2e with a WTT / TTW split; money is per-currency (workspace default AED) and never summed across currencies.
Quick error reference
| Code | Meaning | Likely cause |
|---|---|---|
401 | Unauthorized | Missing/invalid/expired/revoked key or token |
403 | Forbidden | Key/role lacks the required (resource, action) grant |
400 | Bad request | Business rule violated (e.g. publish a disclosure missing required metrics) |
422 | Unprocessable | Request body fails schema validation |