Your API
Your API is Mibo’s canonical trace shape, posted as plain JSON. It’s the path the n8n Testing Node and the Flowise template use under the hood, and the right choice for any hand-rolled integration — curl, your own code, or a stack on a managed host with no OTel access.
If your stack already emits OpenTelemetry, use OTLP instead — fewer moving parts.
The shape
Section titled “The shape”{ "spans": [ { "span_id": "a3f1...", "parent_span_id": null, "name": "Restaurant Reservations Agentflow", "start_time_unix_nano": "1717689600000000000", "end_time_unix_nano": "1717689602450000000", "attributes": { "gen_ai.response.text": "Your table for 4 is booked at 8pm.", "gen_ai.usage.input_tokens": 120, "gen_ai.usage.output_tokens": 80, "http.response.status_code": 200 }, "status": { "code": 1 } } ], "metadata": { "environment": "production", "version": "1.2.0" }}Posted to POST /public/traces. The body has spans at the top level — there is no data wrapper.
Endpoint
Section titled “Endpoint”POST https://api.mibo-ai.com/public/tracesHeaders
Section titled “Headers”| Header | Required | Purpose |
|---|---|---|
x-api-key | Yes | Your Mibo API key. See API Keys & Trace Routing. |
Content-Type | Yes | application/json |
x-request-id | Recommended | Trace’s external identifier — Mibo upserts by this. See Identity. |
Content-Encoding | No | gzip for compressed bodies. Useful for large traces. |
Body fields
Section titled “Body fields”| Field | Required | Description |
|---|---|---|
spans | Yes | Array of canonical spans. See Span shape. |
metadata | No | Free-form key-value object stored with the trace. Set metadata.mibo.platform_id to route to a specific agent. |
externalMetadata | No | Free-form context only Mibo’s API sees (never round-tripped to the runner). |
platformId | No | Explicit agent UUID. Equivalent to metadata.mibo.platform_id at the top level. |
Identity is header-only — externalId is read from x-request-id, never from the body.
Span shape
Section titled “Span shape”interface CanonicalSpan { span_id: string; // unique within the trace parent_span_id?: string | null; // null/omit for root name: string; // user-facing display label — see below attributes?: Record<string, unknown>; start_time_unix_nano?: string; // string-encoded uint64 nanoseconds end_time_unix_nano?: string; status?: { code?: number; message?: string }; // 1 = OK, 2 = ERROR events?: unknown[]; // OTel-style span events; usually empty}span.name invariant
Section titled “span.name invariant”span.name is the user-facing display label of the step — the label you and your end-users see in your tool (n8n workflow node display name, Flowise component label). Never the internal id, never the technical class name.
The node_call assertion does case-insensitive substring matching against span.name, so what your user sees in their tooling is what they write in their assertions.
parent_span_id
Section titled “parent_span_id”Wire the parent graph from your tool’s execution graph. The root span has parent_span_id: null (or omit the field). Children point at their parent’s span_id. Mibo derives the trace’s start/end timing, http status, and aggregate token usage from the root span when available.
If a span is captured but its real parent was filtered out, point it at the nearest captured ancestor instead — don’t leave orphans.
status.code
Section titled “status.code”1— span succeeded.2— span errored. Mibo marks the whole tracefailedif any span hasstatus.code === 2.- Omitted /
0— unset; treated as success.
Attributes the runner reads
Section titled “Attributes the runner reads”These are the GenAI / HTTP semconv keys Mibo evaluates. Emit what you have; missing keys surface as missing instrumentation in the dashboard rather than silent pass/fail.
| Attribute | What it powers |
|---|---|
gen_ai.output.messages | Preferred response source — [{ role, parts: [{ type, content }] }]. The last assistant message’s text parts are concatenated. |
gen_ai.response.text | Fallback response source. Resolution: root → latest AI-orchestrator span → latest span overall. |
gen_ai.input.messages | User input for semantic evaluators. Latest user message wins; falls back to system, then concatenated parts. Read from the AI span. |
gen_ai.prompt | Fallback input source when input.messages is absent. |
gen_ai.system / gen_ai.request.model | Marks the span as the AI orchestrator (biases response/input extraction toward it). |
gen_ai.usage.input_tokens / gen_ai.usage.output_tokens / gen_ai.usage.reasoning.output_tokens | token_limit. Read from the root, summed across descendants if absent. |
gen_ai.tool.name + gen_ai.tool.call.arguments | The span is attached to its parent’s tool_calls. Arguments may be JSON-stringified. |
http.response.status_code | http_status. Read from the root, falls back to any descendant. |
You can also send your own attributes for organizational purposes (flowise.component.type, n8n.node.type, etc.). The dashboard renders them generically; assertions ignore them unless you reference them explicitly.
Identity
Section titled “Identity”Mibo reads externalId from the x-request-id HTTP header:
- Present → create-or-replace by that id. Posting again with the same
x-request-idoverwrites the previous trace. - Absent → server-generated UUID. Always creates a new trace; no idempotency.
The n8n Testing Node sets x-request-id to the n8n execution id. The Flowise template sets it to the chatflow run id. Body fields never carry identity.
Agent resolution
Section titled “Agent resolution”The trace is matched to a Mibo agent in this order:
- If the API key is scoped to a single agent, that agent is used.
- Otherwise the request must include
platformIdat the top level ormetadata.mibo.platform_id. - If neither resolves, the request returns
400 CUSTOM_API_PLATFORM_NOT_FOUNDwith a hint.
OTLP uses a different rule — it routes by the service.name resource attribute matched against an agent’s OTLP service name field, and ignores mibo.platform_id.
Ingestion is one POST per trace
Section titled “Ingestion is one POST per trace”Unlike OTLP (where exporters batch partial spans across multiple POSTs), Your API ingestion is one POST carrying every span. Build the full payload synchronously, then post once. Resending with the same x-request-id replaces the previous trace — there is no merge-by-span_id on this path.
Resilience and error handling
Section titled “Resilience and error handling”Response codes and retries
Section titled “Response codes and retries”201 Createdor200 OK— trace accepted. Passive evaluation is queued automatically.400 VALIDATION_ERROR— your payload doesn’t match the Your API shape or is missing required fields (span_id,name,spans). Check the error details and retry with a corrected payload.400 CUSTOM_API_PLATFORM_NOT_FOUND— your API key doesn’t have access to the specified platform, or your request didn’t specify one. Verify yourplatformIdand API key scope.401 UNAUTHORIZED— API key is missing, invalid, revoked, or expired. Check your credentials.413 PAYLOAD_TOO_LARGE— your trace exceeds 30 MB decompressed. Reduce batch size or filter verbose attributes.500+errors — transient server issue. Retry with exponential backoff (e.g., wait 1s, then 2s, then 4s). Do not treat these as validation errors; your payload may be correct.
Idempotency with x-request-id
Section titled “Idempotency with x-request-id”Include the x-request-id header in all requests to make trace submission idempotent. If your network request times out or you’re unsure whether it succeeded:
- Resend with the same
x-request-id— Mibo will update the existing trace instead of creating a duplicate. No harm done. - Without
x-request-id— every POST creates a new trace, even if it’s the same data. If you retry after a network failure, you’ll get two traces.
This is critical for systems that might retry on error (use the same x-request-id as the original failed request).
Payload compression
Section titled “Payload compression”For large traces, send Content-Encoding: gzip with a gzip-compressed body. Mibo will decompress it before processing. Still subject to the 30 MB decompressed limit.
curl -X POST https://api.mibo-ai.com/public/traces \ -H "x-api-key: $MIBO_API_KEY" \ -H "Content-Type: application/json" \ -H "Content-Encoding: gzip" \ --data-binary @payload.json.gzFull curl example
Section titled “Full curl example”curl -X POST https://api.mibo-ai.com/public/traces \ -H "Content-Type: application/json" \ -H "x-api-key: $MIBO_API_KEY" \ -H "x-request-id: chat-001" \ -d '{ "spans": [ { "span_id": "root-001", "parent_span_id": null, "name": "Restaurant Reservations Agentflow", "start_time_unix_nano": "1717689600000000000", "end_time_unix_nano": "1717689602450000000", "attributes": { "gen_ai.response.text": "Your table for 4 is booked at 8pm.", "gen_ai.usage.input_tokens": 120, "gen_ai.usage.output_tokens": 80, "http.response.status_code": 200 }, "status": { "code": 1 } }, { "span_id": "tool-001", "parent_span_id": "root-001", "name": "create_booking", "attributes": { "gen_ai.tool.name": "create_booking", "gen_ai.tool.call.arguments": "{\"date\":\"2026-06-07\",\"time\":\"20:00\",\"party\":4}" }, "status": { "code": 1 } } ], "metadata": { "environment": "production" } }'A 2xx response means the trace was accepted; passive evaluation runs in the background against your active test cases.
Building canonical payloads from common stacks
Section titled “Building canonical payloads from common stacks”You probably don’t need to write the JSON by hand:
For anything else, just construct the payload in your code and POST it. The shape is small enough to write inline.
Reject paths
Section titled “Reject paths”The endpoint returns 400 for:
- Missing or empty
spans. - Span without
span_idorname. - Body that’s neither OTLP nor Your API.
- Multi-agent API key with no
platformId/metadata.mibo.platform_id.
Legacy proprietary shapes ({ NodeName: { output, ... } }, agentFlowExecutedData, “n8n trace push”, etc.) are not accepted. Migrate to Your API.