MAP Docs
SDKs

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.dev

A 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_acme

If X-Tenant-Id is absent, the tenant is inferred from the API key.

Optional auth modes

ModeHeaderWhen
API keyAuthorization: Bearer map_live_...Default; for service-to-service
Session cookieCookie: map_session=...For browser-originated calls (admin console)
DID-AuthAuthorization: DidAuth signature=...When the caller has no API key but holds the DID private key
Treaty tokenAuthorization: Treaty $treaty_id $signatureCross-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:

FieldTypeRequiredNotes
protocolstringyesCanonical name, e.g. MARC, MIND
versionstringyesSemver, e.g. v1.0.0 or v1 for best-in-major
operationstringyesOne of the protocol's exported operations
inputobjectyesOperation-specific payload
tenant_idstringyesMust 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

codeHTTPEngine error
unknown_protocol404UnknownProtocol
unknown_version404UnknownVersion
rate_limited429RateLimited (with Retry-After)
capability_denied403CapabilityDenied
circuit_open503CircuitOpen
no_endpoint_available503NoEndpointAvailable
policy_denied422ProtocolError::PolicyDenied
invalid_payload422ProtocolError::InvalidPayload
missing_capability403ProtocolError::MissingCapability
adapter_error502ProtocolError::AdapterError
timeout504Timeout
internal_error500Internal

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=value2

On 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:     1735689200

When you hit the limit:

HTTP/2 429 Too Many Requests
Retry-After: 12
X-RateLimit-Limit:     10000
X-RateLimit-Remaining: 0
X-RateLimit-Reset:     1735689212

Retry-After is the recommended back-off in seconds.

Idempotency

For idempotent retries, supply an Idempotency-Key:

Idempotency-Key: my-app-req-abc123

MAP 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-stream
event: 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

On this page