Carbon Zero Docs

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.

flowchart LR C[Your integration] -->|HTTPS + API key| N[Carbon Zero API
/api/v1/*] N --> DB[(Workspace data)] N -. emits .-> Q[Webhook outbox] Q -->|signed POST| R[Your webhook receiver]
Interactive API explorer A live OpenAPI / Swagger UI is served at /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 URLhttps://<your-workspace-domain>/api/v1
Content typeapplication/json (file uploads use multipart/form-data)
AuthX-API-Key: cz_live_… or Authorization: Bearer <token>
ErrorsNon-2xx with a JSON body { "detail": "message" }
EmissionsReported in tCO2e with a WTT / TTW split (Well-to-Tank + Tank-to-Wake → total)
MoneyJSON numbers, per-currency (workspace default AED) — never summed across currencies
DatesDates 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.

Two-factor authentication If the user has 2FA enabled, /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_… or Authorization: Bearer cz_live_… (the cz_live_ prefix disambiguates from a JWT).
  • Revocation is immediate; expires_at is enforced; an API-key principal can call business endpoints but cannot mint other keys.
GET/api-keysList the workspace's keys (api_keys:read)
POST/api-keysMint 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"
Keys work on RBAC-gated routers API keys are accepted by the permission-gated business endpoints (reporting entities, frameworks, submissions, disclosures, scoring, targets, emissions, fuel feeds, tickets, reports, webhooks, …). The 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:

ResourceTypical actionsGuards
reporting_entitiesread, create, update, archive, view_all, assignOrg / subsidiary / facility / supplier / vessel records & hierarchy; owner+branch scoping
frameworksread, manage, publishFrameworks (GRI/SASB/CSRD-ESRS/TCFD/GHG-Protocol) & lifecycle
metricsread, manageThe metric / data-point catalogue per framework
periodsread, manageReporting periods / campaigns (open/closed)
submissionsread, create, update, verifyThe metric-submission ledger; also gates the emissions calculator & fuel feeds
disclosuresread, create, update, approve, publishESG reports & their immutable published snapshot
scoringread, manage, runScoring models, criteria & computed scores
targetsread, managePer-metric goals & progress
reportsread, buildReport builder catalogue, runs & saved definitions
processesread, create, update, publish, executeEvent-driven automation rules & runs
customer_careaccessEntity-360 search & overview (ignores owner/branch scoping)
ticketsread, create, update, assign, manageSupport tickets & comment threads
tenant_settingsread, manageWorkspace settings (currency, timezone, branding, flags)
branchesread, create, update, manage, view_allBranch records & membership
roles / usersread, manage / read, create, update, suspend, impersonateRoles & member management
reference_dataread, manageMaster-data lists (frameworks, units, sectors, countries)
custom_fieldsread, manageCustom-field definitions & values per object type
webhooksread, create, update, deleteOutbound webhook endpoints & deliveries
api_keysread, create, revokeAPI-key management
Platform admin is separate. The cross-tenant operator console (Platform admin) is gated by the is_platform_admin flag via require_platform_adminnot 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-entitiesList entities — filters by type / parent / search (reporting_entities:read)
POST/reporting-entitiesCreate 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/frameworksList frameworks (frameworks:read)
POST/frameworksCreate a framework (frameworks:manage)
PATCH/frameworks/{id}Update / publish (draft → active) a framework (frameworks:manage / publish)
GET/frameworks/{id}/metricsList a framework's metrics (metrics:read)
POST/frameworks/{id}/metricsAdd 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/periodsList periods (periods:read)
POST/periodsOpen 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/submissionsList submissions — filter by period / entity / metric / status (submissions:read)
POST/submissionsUpsert a submission for a (period, entity, metric) triple — emits submission.created / submission.submitted (submissions:create / update)
POST/submissions/{id}/reviewVerify 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/disclosuresList disclosures (disclosures:read)
POST/disclosuresCreate a draft disclosure — emits disclosure.created (disclosures:create)
GET/disclosures/{id}Get one disclosure (with snapshot & history)
POST/disclosures/{id}/transitionAdvance 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/modelsList scoring models (scoring:read)
POST/scoring/modelsCreate a model (weights, tiers) (scoring:manage)
PATCH/scoring/models/{id}Update a model
POST/scoring/models/{id}/criteriaAdd a criterion (numeric bands or a simpleeval expression + weight)
DELETE/scoring/criteria/{id}Delete a criterion
GET/scoring/scoresList computed scores — filter by model / entity / period (scoring:read)
POST/scoring/runRun 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/targetsList targets with live progress (targets:read)
POST/targetsCreate 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/factorsThe emission-factor catalogue (WTT + TTW per unit); lazy-seeds on first use
GET/emissions/activitiesList activity records (computed wtt / ttw / total tCO2e)
POST/emissions/activitiesAdd an activity record — method → factor → inputs (fuel burn / tonne-km / spend) → computed emissions
DELETE/emissions/activities/{id}Delete an activity record
GET/emissions/summaryRoll-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:

SourceMethodData quality
bdn (Bunker Delivery Note)Aprimary
tank_soundingBprimary
mfm (mass flow meter)Cprimary
ams_feed (automated monitoring)Cprimary
noon_report— (no auditable method)estimated
manualestimated

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-feedIdempotent batch ingest of fuel observations (unique tenant+entity+source+source_ref); computes TTW + WTT
GET/emissions/fuel-feed/recordsList ingested fuel observations (with method + data-quality tags)
POST/emissions/fuel-feed/bdnRecord a Bunker Delivery Note (the auditable "fuel uplifted" anchor)
GET/emissions/fuel-feed/bdnList 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/rollupCollapse 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/searchSearch any entity (ignores scoping) (customer_care:access)
GET/customer-care/entities/{id}/overviewThe 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}/notesAdd a care note to an entity
GET/ticketsList tickets (tickets:read)
POST/ticketsOpen 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}/commentsAdd 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/catalogueThe available sources + whitelisted fields (reports:read)
POST/report-builder/runRun a report (source + fields + filters) (reports:build)
POST/report-builder/exportExport the result as CSV
GET POST/report-builder/definitionsList / save report definitions
GET/automation/event-typesThe webhook events a rule may trigger on (processes:read)
GET POST/automation/rulesList / create rules (trigger event, condition, action) (processes:create)
PATCH DELETE/automation/rules/{id}Update / delete a rule (processes:update)
GET/automation/runsThe 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/summaryEntity counts by type, framework / period counts, submission completeness, disclosure pipeline, score avg + tier distribution, active targets (branch∩owner scoped)
GET PATCH/tenants/settingsWorkspace settings — currency, timezone, date_format, branding, flags (defaults AED + Asia/Dubai + dd/mm/yyyy)
POST/tenants/registerRegister a new workspace (public)
GET/rolesList / POST create roles; PATCH / DELETE /roles/{id} (roles:read / manage); /roles/permissions lists the catalogue
GET/roles/members/listList members; /members/{id}/role (PATCH), /deactivate, /reactivate, /reset-password (users:*)
POST/users/inviteInvite a member; /invitations (list), /invite/{id} (DELETE), /invite/accept
GET/branchesList / create branches; /accessible, /{id}/members (branches:*)
GET PUT/maker-checker/configPer-module dual-control config; /requests to list, /requests/{id}/approve|reject|cancel (checker ≠ requester)
GET/reference-data/listsMaster-data lists + items (frameworks / units / sectors / countries, incl. UAE & maritime) (reference_data:*)
GET/webhooksRegister / list / update / delete endpoints; /event-types, /{id}/deliveries, /roll-secret, /test (webhooks:*)
GET/billingPlan + 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
Custom fields. A custom-objects engine holds a registry of custom-field definitions per object type (e.g. 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_adminseparate 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/statsCross-tenant platform statistics
GET/admin/tenantsList all tenants; /tenants/{id} for detail
POST/admin/tenants/{id}/planSet a tenant's plan; /trial to set trial state
POST/admin/tenants/{id}/suspendSuspend (→ billing canceled / locked; business endpoints 402) / /reactivate
POST/admin/tenants/{id}/impersonateOwner impersonation (return via /auth/impersonate/stop)
GET PUT/admin/plans · /plan-configThe 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/schemaRead-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

sequenceDiagram participant B as Business event (e.g. publish) participant O as Outbox (same DB txn) participant D as Dispatcher (every minute) participant R as Your receiver B->>O: Write delivery row (commits with the disclosure) D->>O: Claim due deliveries D->>R: POST signed JSON (HMAC-SHA256) alt 2xx R-->>D: 200 OK D->>O: mark delivered else failure / timeout R-->>D: non-2xx / error D->>O: retry w/ backoff → dead end

The request & envelope

Each delivery is an HTTP POST with these headers and a stable envelope:

HeaderMeaning
X-CarbonZero-EventThe event type (also in body as event).
X-CarbonZero-DeliveryUnique delivery / event id — your idempotency key.
X-CarbonZero-SignatureHMAC-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 */ }
}
Verify against the raw bytes Verify the signature against the raw bytes exactly as received — do not re-serialize the parsed JSON, or whitespace/key-order differences will break the match.

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_at if you need order.
  • HTTPS only, publicly reachable (private/internal addresses are rejected).

Event types

Eventdata
entity.createdA reporting entity was created.
entity.updatedA reporting entity's fields changed.
entity.archivedA reporting entity was archived.
period.openedA reporting period / campaign was opened.
period.closedA reporting period was closed.
submission.createdA metric submission was created (draft) for a period × entity × metric.
submission.submittedA submission was submitted for review.
submission.verifiedA submission was verified.
submission.overdueAn expected submission passed its due date.
disclosure.createdA draft disclosure was created.
disclosure.submittedA disclosure entered review.
disclosure.approvedA disclosure was approved.
disclosure.publishedA disclosure was published with an immutable metric snapshot.
disclosure.sent_backA disclosure was returned for changes.
score.computedAn ESG score was (re)computed — score, tier, per-criterion breakdown.
score.below_thresholdA computed score fell below a configured threshold.
target.breachedA target's progress crossed into breach.
ticket.createdA support ticket was opened.
ticket.assignedA ticket was assigned to a member.
ticket.resolvedA ticket was resolved.
ticket.commentedA comment was added to a ticket thread.
ticket.overdueA ticket passed its SLA / due date.

Webhook endpoints are managed under /webhooksGET /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:

sequenceDiagram participant I as Your service participant API as Carbon Zero API participant WH as Your webhook receiver I->>API: POST /reporting-entities (create entity) I->>API: POST /frameworks + /frameworks/{id}/metrics I->>API: POST /periods (open FY2025) I->>API: POST /submissions (upsert per entity × metric) I->>API: POST /submissions/{id}/review (verify) API--)WH: submission.verified (signed) I->>API: POST /scoring/run API--)WH: score.computed (signed) I->>API: POST /disclosures + /disclosures/{id}/transition (publish) API--)WH: disclosure.published (signed) → immutable snapshot
  1. Create the reporting entityPOST /reporting-entities (org / subsidiary / facility / supplier / vessel).
  2. Define the framework + metricsPOST /frameworks, then POST /frameworks/{id}/metrics for each data point; publish the framework to active.
  3. Open a periodPOST /periods (e.g. FY2025).
  4. Upsert submissionsPOST /submissions per (period, entity, metric); re-posting updates in place.
  5. VerifyPOST /submissions/{id}/review; listen for submission.verified.
  6. Run scoringPOST /scoring/run → an EsgScore (0–100 → tier); listen for score.computed.
  7. Publish a disclosurePOST /disclosures then POST /disclosures/{id}/transition through to published; act on disclosure.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:

sequenceDiagram participant V as Vessel / shore system participant API as Carbon Zero API V->>API: POST /emissions/fuel-feed (noon reports, MFM, AMS — idempotent) V->>API: POST /emissions/fuel-feed/bdn (Bunker Delivery Notes) V->>API: POST /emissions/fuel-feed/reconcile (Σfeed vs ΣBDN, 5%) V->>API: POST /emissions/fuel-feed/rollup (→ ActivityRecord) Note over API: appears in GET /emissions/summary → scoring → disclosure
  1. Ingest fuel observationsPOST /emissions/fuel-feed (idempotent on tenant+entity+source+source_ref); each computes TTW + WTT.
  2. Record BDNsPOST /emissions/fuel-feed/bdn as the auditable "fuel uplifted" anchor.
  3. ReconcilePOST /emissions/fuel-feed/reconcile compares Σ feed vs Σ BDN per vessel × period, flags variance beyond 5%.
  4. Roll upPOST /emissions/fuel-feed/rollup collapses the reconciled feed into one ActivityRecord per vessel × period × fuel — it then appears in GET /emissions/summary and 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 its ActivityRecord in place — safe for at-least-once delivery.
  • A disclosure's published snapshot is immutable — later submission changes never alter a published report.
  • Treat webhook data as 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

CodeMeaningLikely cause
401UnauthorizedMissing/invalid/expired/revoked key or token
403ForbiddenKey/role lacks the required (resource, action) grant
400Bad requestBusiness rule violated (e.g. publish a disclosure missing required metrics)
422UnprocessableRequest body fails schema validation