Skip to Content
IntegrationsOTLP

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/traces

This 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.json

Normalization

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:

AttributeTypeMeaning
morse.outcome.successboolDid this run achieve its goal? Required to declare an outcome.
morse.outcome.labelstringOptional 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/traces with 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’s llm span type automatically.
  • Set morse.outcome.success on 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.
  • 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.
Last updated on