Wire
The HTTP envelope, auth headers, trace context, error mapping. Everything a non-shipped language needs to build a MAP client.
The MAP wire protocol is intentionally small. One endpoint, one request shape, one response shape, four authentication mechanisms. You can build a working client in any language in a few hundred lines.
Endpoint
POST /v1/dispatch HTTP/2
Host: api.multiagentic.devA single endpoint dispatches every protocol operation. No per-protocol REST surface — MAP is dispatch-based, not resource-based.
Authentication
The Authorization header carries the bearer token issued by the CLI or your API key:
Authorization: Bearer map_live_abcdef1234567890...Plus a header that identifies the caller agent:
X-Agent-Did: did:oas:l1fe:agent:0xa3f9c1a4b5...Plus a header that identifies the tenant (organization scope):
X-Tenant-Id: org_acmeIf X-Tenant-Id is absent, the tenant is inferred from the API key.
Optional auth modes
| Mode | Header | When |
|---|---|---|
| API key | Authorization: Bearer map_live_... | Default; for service-to-service |
| Session cookie | Cookie: map_session=... | For browser-originated calls (admin console) |
| DID-Auth | Authorization: DidAuth signature=... | When the caller has no API key but holds the DID private key |
| Treaty token | Authorization: Treaty $treaty_id $signature | Cross-organization calls |
DID-Auth requires the caller to first run MACS::auth_negotiation then MACS::verify_response to receive a short-lived stamped envelope. See MACS.
Request envelope
POST /v1/dispatch
Content-Type: application/json
Authorization: Bearer $TOKEN
X-Agent-Did: did:oas:l1fe:agent:0x...
X-Tenant-Id: org_acme
traceparent: 00-4f81b3a000000000aabbccdd00112233-01020304050607080-01
{
"protocol": "MARC",
"version": "v1.0.0",
"operation": "reasoning_task",
"input": {
"intent": "is the treaty enforceable?",
"budget": { "tokens": 8000, "deadline_ms": 12000 }
},
"tenant_id": "org_acme"
}Fields:
| Field | Type | Required | Notes |
|---|---|---|---|
protocol | string | yes | Canonical name, e.g. MARC, MIND |
version | string | yes | Semver, e.g. v1.0.0 or v1 for best-in-major |
operation | string | yes | One of the protocol's exported operations |
input | object | yes | Operation-specific payload |
tenant_id | string | yes | Must match X-Tenant-Id or be on an active MOAT treaty |
Response envelope
Success
HTTP/2 200 OK
Content-Type: application/json
X-Map-Correlation-Id: 4f81b3a-1234-5678-9abc-def012345678
X-Map-Audit-Head: 0x4f81b3acdef...
traceparent: 00-...
X-RateLimit-Remaining: 9483
X-RateLimit-Reset: 1735689200
{
"output": { /* the protocol's response data */ }
}output is the unwrapped Response::data from the protocol. Response::metadata (if any) flows through the audit chain — recover via MAX::traceability_graph keyed on the correlation ID.
Error
HTTP/2 403 Forbidden
Content-Type: application/json
X-Map-Correlation-Id: ...
{
"error": {
"code": "capability_denied",
"message": "missing capability map.marc.reasoning_task",
"context": {
"protocol": "MARC",
"operation": "reasoning_task",
"tenant_id": "org_acme"
},
"correlation_id": "..."
}
}Error code → HTTP mapping
code | HTTP | Engine error |
|---|---|---|
unknown_protocol | 404 | UnknownProtocol |
unknown_version | 404 | UnknownVersion |
rate_limited | 429 | RateLimited (with Retry-After) |
capability_denied | 403 | CapabilityDenied |
circuit_open | 503 | CircuitOpen |
no_endpoint_available | 503 | NoEndpointAvailable |
policy_denied | 422 | ProtocolError::PolicyDenied |
invalid_payload | 422 | ProtocolError::InvalidPayload |
missing_capability | 403 | ProtocolError::MissingCapability |
adapter_error | 502 | ProtocolError::AdapterError |
timeout | 504 | Timeout |
internal_error | 500 | Internal |
Trace context
MAP propagates W3C Trace Context throughout. If you send a traceparent header, MAP threads it through every internal hop and every audit record.
traceparent: 00-{trace-id}-{parent-id}-{flags}
tracestate: vendor1=value1,vendor2=value2On the response, MAP echoes the traceparent so you can correlate.
Rate-limit headers
Every successful response carries:
X-RateLimit-Limit: 10000
X-RateLimit-Remaining: 9483
X-RateLimit-Reset: 1735689200When you hit the limit:
HTTP/2 429 Too Many Requests
Retry-After: 12
X-RateLimit-Limit: 10000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1735689212Retry-After is the recommended back-off in seconds.
Idempotency
For idempotent retries, supply an Idempotency-Key:
Idempotency-Key: my-app-req-abc123MAP caches the response keyed by (tenant_id, idempotency_key) for 24 hours. The second request with the same key returns the cached response without re-dispatching.
This is the only safe way to retry state-changing operations.
Streaming responses
For long-running operations (some MARC::reasoning_task calls, all MACE::deliberate), the engine streams Server-Sent Events:
POST /v1/dispatch?stream=1
Accept: text/event-streamevent: progress
data: { "stage": "world_model_bound", "elapsed_ms": 240 }
event: progress
data: { "stage": "deliberating", "delegates_voted": 3 }
event: result
data: { "output": { ... }, "audit_head": "0x..." }Streamed events carry the same traceparent and X-Map-Correlation-Id as a single-shot response.
A minimal client in 60 lines
Python example:
import os, json, urllib.request
class MapClient:
def __init__(self, *, api_url, api_key, agent_did, tenant_id):
self.api_url = api_url.rstrip('/')
self.api_key = api_key
self.agent_did = agent_did
self.tenant_id = tenant_id
def dispatch(self, protocol, version, operation, input, idempotency_key=None):
body = json.dumps({
'protocol': protocol,
'version': version,
'operation': operation,
'input': input,
'tenant_id': self.tenant_id
}).encode('utf-8')
req = urllib.request.Request(
self.api_url + '/v1/dispatch',
data=body, method='POST',
headers={
'Authorization': f'Bearer {self.api_key}',
'X-Agent-Did': self.agent_did,
'X-Tenant-Id': self.tenant_id,
'Content-Type': 'application/json',
**({'Idempotency-Key': idempotency_key} if idempotency_key else {})
}
)
try:
with urllib.request.urlopen(req) as resp:
return json.load(resp)['output']
except urllib.error.HTTPError as e:
raise MapError(json.load(e)['error'])
class MapError(Exception):
def __init__(self, err): self.err = err
def __str__(self): return f'{self.err["code"]}: {self.err["message"]}'This is the entire wire surface. Anything more sophisticated — retry logic, trace propagation, validation — is application policy. The MAP wire is intentionally that small.
See also
- Engine handle_request — the server-side type
- Engine types —
HandleRequestInput,Response, errors - Rust SDK
- TypeScript SDK