OTLP
Already emit OpenTelemetry traces? You don’t need our SDK for them. Morse accepts standard OTLP traces directly, so an existing OTel pipeline can point at Morse without adding an SDK dependency to your app.
How it works
Morse’s ingestion is protocol-agnostic: the SDK path and the OTLP path both end up in the same persist function, so a trace looks identical in the UI regardless of how it arrived.
Endpoint
POST https://api.morsehq.dev/api/v1/otlp/v1/tracesThis is HTTP only — there is no gRPC ingest endpoint today. If your exporter is configured for OTLP/gRPC, switch it to OTLP/HTTP and point it at the path above.
Configure it with the signal-specific environment variable, not the generic one. Stock OTLP
exporters append /v1/traces to OTEL_EXPORTER_OTLP_ENDPOINT, which would send your spans to
/api/v1/otlp/v1/traces/v1/traces and 404. OTEL_EXPORTER_OTLP_TRACES_ENDPOINT is used verbatim:
export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="https://api.morsehq.dev/api/v1/otlp/v1/traces"
export OTEL_EXPORTER_OTLP_TRACES_HEADERS="Authorization=Bearer%20$MORSE_API_KEY"
export OTEL_EXPORTER_OTLP_TRACES_PROTOCOL="http/protobuf"Header values are URL-decoded by the OpenTelemetry SDKs, so encode the space in
Bearer YOUR_KEY as %20.
Content types
The endpoint accepts either:
application/x-protobuf— standard OTLP protobuf, converted internally and normalized the same way as JSON.application/json— OTLP JSON.
A request with no Content-Type is treated as JSON. Errors are explicit, so your exporter sees a
failed batch instead of a silent success:
- 415 — any other content type. The logs (
/api/v1/otlp/v1/logs) and metrics (/api/v1/otlp/v1/metrics) endpoints accept JSON only for now and also return 415 for protobuf. - 400 — a body that cannot be parsed, or JSON that is not an object.
Naming the agent
By default a trace’s agent name comes from the resource’s service.name. When that is missing or
is OTel’s unknown_service default, Morse uses the root span’s name. To set the agent name
explicitly per trace, put a morse.agent attribute on the root span. It wins over
service.name — useful when one process runs several agents under a single OTEL_SERVICE_NAME.
The value is trimmed and capped at 255 characters, and it is ignored on non-root spans.
with tracer.start_as_current_span("handle-ticket") as span:
span.set_attribute("morse.agent", "support-agent")Auth
Send your Morse API key the same way you would for the SDK — as a bearer API key.
curl -X POST https://api.morsehq.dev/api/v1/otlp/v1/traces \
-H "Authorization: Bearer $MORSE_API_KEY" \
-H "Content-Type: application/json" \
-d @spans.jsonNormalization
Every OTLP span is mapped into the same typed span model the SDK produces — agent, llm, tool,
http, db, or custom — before it’s persisted. Priority order: an explicit morse.span_type
attribute, then gen_ai.* attributes, then db.system, then http.method / rpc.system, falling
back to the OTLP span kind.
Declaring outcomes
Morse can tell you how much your agents cost the moment spans arrive. It cannot tell you
whether they worked — nothing in the OpenTelemetry semantic conventions expresses “this
run achieved what the user wanted”. A span that ends STATUS_CODE_OK only means the code
did not raise.
So you declare it, with two span attributes:
| Attribute | Type | Meaning |
|---|---|---|
morse.outcome.success | bool | Did this run achieve its goal? Required to declare an outcome. |
morse.outcome.label | string | Optional short reason — "resolved", "escalated", "refund_issued". |
Why it matters
A trace with no morse.outcome.success is excluded from every production-success
metric in Morse — the success rate on your agent pages, and cost-per-successful-outcome
(CPSO), the number behind the verdict. A trace carrying only cost is a trace Morse can
bill-analyze but cannot judge.
Declaring an outcome sets that trace’s outcome_provenance to developer — the same
trust tier the Morse SDK’s set_outcome() earns. It is the highest-trust tier available
from instrumentation, because you are the one who knows what success meant.
morse.outcome.label on its own does not do this. The success flag is what earns the
developer tier; a label with no success flag leaves the trace at implicit and still
excluded. Set morse.outcome.success whenever you set a label.
Any span, any batch
Put the attributes on whichever span knows the answer. You do not have to find the root span, and you do not have to hold a span open until the outcome is known — the attributes are read off every span, and the last one to declare wins when the trace is assembled, even if it arrives in a later export batch.
In practice that means a tool span deep in the run can declare failure, or a final evaluation step can declare success after the agent span has already been exported.
Examples
from opentelemetry import trace
tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span("support-agent") as span:
result = run_agent(ticket)
span.set_attribute("morse.outcome.success", result.resolved)
span.set_attribute("morse.outcome.label", "resolved" if result.resolved else "escalated")import { trace } from "@opentelemetry/api";
const tracer = trace.getTracer("support-agent");
await tracer.startActiveSpan("support-agent", async (span) => {
const result = await runAgent(ticket);
span.setAttribute("morse.outcome.success", result.resolved);
span.setAttribute("morse.outcome.label", result.resolved ? "resolved" : "escalated");
span.end();
});import "go.opentelemetry.io/otel"
import "go.opentelemetry.io/otel/attribute"
tracer := otel.Tracer("support-agent")
ctx, span := tracer.Start(ctx, "support-agent")
defer span.End()
result := runAgent(ticket)
span.SetAttributes(
attribute.Bool("morse.outcome.success", result.Resolved),
attribute.String("morse.outcome.label", label(result)),
)Send morse.outcome.success as a real boolean, not the string "true" — a string is
ignored and the trace stays implicit.
There’s no separate “OTLP view” in the product — traces that arrive this way show up in Traces exactly like SDK-originated traces, with the same filters, the same verdict strip, and the same eval/context tabs.
How to use it
- Point your existing OTel SDK’s OTLP/HTTP exporter at
https://api.morsehq.dev/api/v1/otlp/v1/traceswith your Morse API key as the bearer token. - Send either protobuf or JSON — don’t reconfigure your exporter’s encoding just for Morse.
- If you use semantic-convention
gen_ai.*attributes for LLM calls, they map straight to Morse’sllmspan type automatically. - Set
morse.outcome.successon a span so Morse can judge the run and not just price it — see Declaring outcomes. Without it the trace is excluded from success rate and CPSO.
Related
- Traces — where OTLP-ingested spans show up, alongside SDK-originated ones, and where a declared outcome appears.
- MCP endpoint — query that same trace data from an MCP client.