Sending Traces
Mibo accepts traces on a single endpoint, POST /public/traces, with two supported request shapes. Both produce the same canonical trace internally — pick whichever fits your stack.
The two paths
Section titled “The two paths”Both paths are first-class. Neither is a workaround for the other.
Which one fits your stack
Section titled “Which one fits your stack”| You’re on… | Use |
|---|---|
| A service emitting OTel via the Anthropic / OpenAI / Traceloop / OpenLLMetry SDKs | OTLP |
n8n self-hosted ≥ 2.19 with N8N_OTEL_TRACING_ENABLED=true | OTLP |
| n8n Cloud (no host-level OTel) | Your API — see Send traces from n8n for all options |
| Flowise self-hosted with native OTel enabled | OTLP |
| Flowise Cloud (no host-level OTel) | Your API via the Flowise template |
| Anything else with an HTTP endpoint — LangChain agents, your own code, scripts, curl | Your API |
If you can choose OTLP, it usually wins — your existing exporter is already batched, retried, and instrumented. Your API exists so users on managed hosts (n8n Cloud, Flowise Cloud, anything where you can’t touch host env vars) get the same first-class trace ingestion.
What’s the same across both paths
Section titled “What’s the same across both paths”- Endpoint:
POST https://api.mibo-ai.com/public/traces - Auth:
x-api-keyheader. See API Keys & Trace Routing. - Identity:
x-request-idHTTP header. Present → upserts the trace with that id. Absent → server-generated UUID, always creates a new trace. - Agent resolution: if the API key is scoped to a single agent, that agent is used. Otherwise the request must carry
platformId(top level) ormetadata.mibo.platform_id(Your API) /mibo.platform_idresource attribute (OTLP). - Canonical span model: spans with
span_id,parent_span_id,name,attributes, optional timing and status.span.nameis always the user-facing display label of the step.
What’s different
Section titled “What’s different”| OTLP | Your API | |
|---|---|---|
| Body shape | resourceSpans[].scopeSpans[].spans[] (OTel envelope) | { spans: [...] } (flat) |
| Attributes encoding | OTel key/value list ({ key, value: { stringValue: "..." } }) | Plain JSON object |
| Batching | Multi-POST: server merges by traceId/span_id | One POST per trace; create-or-replace by externalId |
| Best for | Long-lived processes, SDK-instrumented services | Synchronous flows, short scripts, managed-host integrations |
The runner reads the same attributes on both paths — gen_ai.response.text, gen_ai.usage.input_tokens, gen_ai.tool.name, http.response.status_code, and the rest of the GenAI / HTTP semconv keys. Your assertions don’t change.
What every span should carry
Section titled “What every span should carry”Whichever path you send on, these are the attributes assertions look up. Emit what you have; missing data surfaces as missing instrumentation in the dashboard rather than silent pass/fail.
| Attribute | What it powers |
|---|---|
gen_ai.response.text | Semantic assertions, response_regex, default text extraction |
gen_ai.output.messages | Same as above when present (preferred over response.text) |
gen_ai.usage.input_tokens / output_tokens / reasoning.output_tokens | token_limit |
gen_ai.tool.name + gen_ai.tool.call.arguments | tool_call (the span becomes a child tool call of its parent) |
http.response.status_code | http_status |
Span name | node_call (case-insensitive substring match) |
Root span start/end_time_unix_nano | response_time.max_ms |
See Assertion Reference for the full mapping.