Overview
The HTTP API carries the same six KernelPort calls, the same three outcomes and the same proof records as the SDK and the MCP server. It adds what a networked caller needs: tokens with scopes, idempotency keys, a response envelope you can branch on, and HMAC-authenticated webhooks for writes that wait on a person.
- Base path
/v1on your elan1 Govern endpoint.- Mode
- Mode B · Attested advisory Evaluated against your rules and recorded. Your system holds the write credential.
- Format
- JSON request and response bodies; times in ISO 8601 with the time-zone offset.
- Families
act,roandctl, each reached with its own kind of token.- Callers
- Third-party agents and services; elan1 Work Attach; elan1 Decide, whose writes go to Govern as intents; elan1 Run tooling, which fails closed if elan1 is unreachable.
curl -X POST "https://<your elan1 Govern endpoint>/v1/assert" \
-H "Authorization: Bearer $ELAN1_ACT_TOKEN" \
-H "X-Tenant: tnt_specimen_01" \
-H "Idempotency-Key: wr_specimen_7c3" \
-H "Content-Type: application/json" \
-d '{
"kernelport": "v1",
"call": "assert",
"write_id": "wr_specimen_7c3",
"write_type": "purchase_invoice.post",
"declaration": { "id": "decl_gst_purchase", "version": 4 },
"actor": { "id": "agt_specimen_ap01", "kind": "agent" },
"record": { "id": "pinv_specimen_0418", "expected_version": 0 },
"payload": { "vendor": "vnd_specimen_0091", "amount_inr": 482000, "hsn": "8483", "gst_rate_pct": 12 }
}'Payload fields are examples, not the published schema.
Endpoint families
Endpoints are grouped in three families. A token belongs to one family, so a credential leaked from one job cannot be used for another.
Send write intents up the Right Path, withdraw them, and read their outcomes. The only family that can lead to a change in a system of record.
Declarations, write types, obligations and proof records; chain verification; scope previews. Nothing in this family writes.
Register declaration versions and manage the integration. A ctl token cannot read any record.
Endpoints
The shape of the surface. Paths here are illustrative; the exact paths are in the KernelPort v1 reference. The family, the call behind each endpoint and its effect are as shown.
| Endpoint | Family | Call | Effect |
|---|---|---|---|
POST /v1/assert | act | assert | Sends a write intent. Answers committed, refused or escalated. Writes a proof record. |
POST /v1/refuse | act | refuse | Records a typed refusal: an approver's no, or a withdrawal. Metered at 0.4 of a governed action. |
GET /v1/writes/{write_id} | act | — | The outcome, sequence and digest of your own write intent. |
GET /v1/obligations/{id} | ro | — | A held write: who must decide, and when it expires. |
GET /v1/records/{seq} | ro | — | One proof record: the sixteen Seal v6 fields. |
GET /v1/records?from_seq=&to_seq= | ro | — | A stretch of chain, in seq order. Use it to fill a webhook gap. |
POST /v1/verify | ro | verify | Recomputes digests and walks the chain. At L1, verification needs elan1. |
GET /v1/scope | ro | scope | Advisory blast radius and autonomy for an actor, write type and record. |
GET /v1/declarations/{id}/versions/{v} | ro | — | A declaration version: its seven blocks and digest. |
POST /v1/define | ctl | define | Registers a declaration version. A governed write under dual control. |
POST /v1/webhooks | ctl | — | Registers a webhook receiver and its HMAC secret. |
seal has no endpoint: only the kernel seals. You see its result in every proof block.
Authentication and scopes
Every request carries a bearer token. A token is scoped to one tenant and one family, and the tenant is restated in the X-Tenant header so a misrouted request fails rather than reaching the wrong chain.
| Token | Family | May | May not |
|---|---|---|---|
| Tenant token, act scope | act | Send and withdraw write intents; answer an obligation when held by the named approver. | Approve a request it made. An agent token can never approve. |
| Read-only token | ro | Read records, declarations and obligations; verify; preview scope. | Write anything. |
| Control-plane token | ctl | Register declaration versions; manage webhooks. | Read any record. |
- Chains never cross tenants. A token for one tenant cannot read or write another’s, and a cross-domain query is not slow; it is impossible.
- An actor without a grant resolves to A0 Observe.
- Approval separation is checked on every answer: no approval where the approver is the requester, and none by an agent token (conformance test C11).
Versions and pinning
Three versions travel with every write, and all three end up in the record: the API version (kernelport/v1), the declaration version (F07) and the policy version (F08).
- Pin in production. Production keys must name an explicit declaration version. “latest” is rejected as a contract error (conformance test C13).
- Replay is deterministic. The same intent, versions and as-of time give the same decision on replay (C4).
- Versions only move forward. A declaration version is never edited. Old records keep pointing at the rules that applied to them.
Idempotency by write_id
Every write intent carries a write_id, and the same value goes in the Idempotency-Key header. It is the identity of the intent everywhere: field F04 on the proof record, the key on the meter event, and the reference on an invoice line — an invoice line without a write_id cannot exist.
# First attempt: the connection drops before the answer arrives.
POST /v1/assert Idempotency-Key: wr_specimen_7c2 → (timeout)
# Retry with the same write_id.
POST /v1/assert Idempotency-Key: wr_specimen_7c2 → 200
{
"outcome": "committed",
"replayed": true,
"proof": { "seq": 1042, "digest": "sha256:f2423aa3…5aca2c18" },
"meter": { "governed_actions": 1 } // the original event, not a second one
}- Create it once.
Generate the write_id when the intent is formed, and store it before you send anything.
- Retry with the same one.
After a timeout or a dropped connection, send the same intent with the same write_id. Replaying returns the original outcome, sequence and digest, and no second meter event (C7).
- Never reuse it for a new intent.
An outcome is final for its write_id. After a refusal, fix the cause and send a new intent with a new write_id.
How the API answers a reused write_id with a different payload is set out in the customer reference. Design as if it is refused.
Response envelope
Every call that writes answers with the same envelope. Branch on outcome, then on refusal.class. Show refusal.code and its plain words to a person; never parse them.
{
"outcome": "committed | refused | escalated",
"rung": 0-9, // where it ended; "0b" is STOP
"reason": "plain words",
"refusal": { // present when outcome is "refused"
"code": "<from the frozen register>",
"class": "rule | platform",
"reason": "plain words",
"remedy": "what to fix",
"contact": "who to ask"
},
"obligation": "OB-…", // present when outcome is "escalated"
"record": { "id": "…", "version": 0 },
"proof": { "seal": "elan1.proof.seal.v6", "seq": 0, "digest": "sha256:…" },
"meter": { "governed_actions": 0, "inr": 0 } // 1 when committed; 0.4 when refused
}| Field | When | Meaning |
|---|---|---|
outcome | Always | committed, refused or escalated. |
rung | Always | The rung where the intent ended, 0b STOP to 9 METERED. |
refusal | Refused | A typed code from the closed register, its class, and plain words: what happened, what to fix, who to ask. See Errors and refusals. |
obligation | Escalated | The held write a named person must answer before it expires. |
record | Always | The target record and its version after the write. On a refusal, a zeroed id and version 0. |
proof | Always | The seal, sequence and digest of the proof record this outcome wrote. |
meter | Always | Governed actions counted: 1 when committed, 0.4 when refused, 0 while escalated. Beyond the allowance, a refusal costs 40% of your level's rate, for example ₹0.64 at L1. |
Status codes
An outcome is an answer, not an error. All three outcomes come back as 200 with the envelope; a refusal is a successful call that said no. Status codes outside 200 mean the call itself did not produce an outcome.
| Status | Means | Proof record | Billed |
|---|---|---|---|
200 | Committed Refused Escalated | Written | A commit: one governed action. A refusal: 0.4 of one. An escalation: nothing while held. |
4xx | Contract error: the request did not meet the contract — shape, token, tenant, version pin. | None | No |
5xx | Our failure. Nothing is allowed. Where the chain writer is reachable, a platform refusal is recorded. | When reachable | Never |
HTTP/1.1 422 Unprocessable Content
{
"error": {
"class": "contract",
"reason": "declaration.version must be a number. \"latest\" is not accepted on a production key.",
"field": "declaration.version"
}
}
# No Right Path, no proof record, no meter event.The precise 4xx and 5xx codes are in the customer reference. Calls under fusion.*/v1 in elan1 Run answer a refusal at the edge with 409, which costs 40% of one governed action; that is a different surface, described in fusion.*/v1 contracts.
Webhooks
A write that waits on a person resolves later. Rather than poll, register a receiver: elan1 sends an event when an obligation resolves and when a record is sealed for your integration. Webhooks are HMAC-authenticated and carry seq, so a gap in delivery is detectable.
POST https://<your receiver>/elan1/events
Content-Type: application/json
X-Signature: t=2027-07-14T10:42:08+05:30, v1=<hex HMAC-SHA256>
{
"event": "obligation.resolved",
"tenant_id": "tnt_specimen_01",
"seq": 1042,
"write_id": "wr_specimen_7c2",
"obligation": "OB-specimen-0412",
"outcome": "committed",
"proof": { "seq": 1042, "digest": "sha256:f2423aa3…5aca2c18" }
}Verify, then check the sequence
import { createHmac, timingSafeEqual } from "node:crypto";
// Illustrative. Exact header name, signed string and encoding are in the KernelPort v1 reference.
export function verifyWebhook(rawBody: string, header: string, secret: string): boolean {
const parts = Object.fromEntries(header.split(",").map((p) => p.trim().split("=", 2)));
const expected = createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(parts.v1 ?? "", "hex");
return a.length === b.length && timingSafeEqual(a, b);
}
// Then check seq: each event's seq should follow the last one you stored.
// A jump means a missed delivery — read the gap back from the records endpoint.- Reject a delivery whose signature does not match; it did not come from elan1.
- Store the last
seqyou processed. If the next one jumps, read the missing stretch fromGET /v1/recordsbefore acting. - Deliveries can repeat. Treat
write_idas the key, and processing as idempotent.
Mode B semantics
Over HTTP, elan1 Govern evaluates and records; your system holds the credential and makes the write. We call that attested advisory and never say a write was blocked. The order of operations is what makes the record mean something.
- Ask first.
Call
assertbefore your system writes. A write made without asking leaves no record. - Write only on committed.
Your writer acts on committed and nothing else. Escalated means wait for the webhook; refused means do not write.
- Fail closed.
No answer is not an allow. If elan1 is unreachable, do not write — as elan1 Run tooling does.
- Keep the write_id with the change.
Carry it into your system of record so the change, the proof record and the meter event reconcile both ways (C10).
What the customer reference covers
- Paths, header names and the exact 4xx and 5xx codes.
- Envelope field names beyond
outcome,write_idand the Seal v6 fields. - Webhook event names, the signed string and the signature header.
- Pagination and rate limits on the read-only family.
- How a Mode B caller reports the resulting record version back for reconciliation.
All of these are fixed in KernelPort v1 and the record shape, and are listed in the full reference customers receive with their workspace.