Axis documentation

Everything you need to send your first span, understand what the dashboard shows you, and instrument a real multi-step process.

What is Axis?

Modern business processes — a checkout, an onboarding, a nightly billing run — are chains of steps spread across services you don't fully control: payment APIs, databases, schedulers, email providers, chat bots. When one link breaks, the whole process breaks, and no single tool shows you which link it was.

Axis gives you that process-level view. Your systems report each step as a span; spans that share a trace ID are stitched into one execution — one run of your process — which you can watch on a dashboard as a table, a timeline waterfall, or an interactive graph with each step colored by its outcome. Declare the expected shape of a process and Axis will even tell you when a step never ran at all.

Core concepts

Span — the atomic record of one step: which step, which execution, when it started and ended, whether it succeeded, plus any metadata you attach. One HTTP call, one DB write, one email send — each is a span.

Trace ID / execution — a correlation ID you choose (an order ID works great). Every span carrying the same trace ID belongs to the same execution of your process.

Virtual endpoint — a named collection point you create in the dashboard, usually one per third-party service ("payments", "email"). Spans reference it via endpoint_id; process definitions are built from them.

Process definition — an optional declaration of a process's expected steps (as endpoints, in order). With strict mode on, executions get a completeness verdict: missing, failed, and out-of-order steps.

Collection modes — today spans are collected passively: your systems call third parties directly and report telemetry out-of-band, so Axis is never in your critical path. An active proxy mode (route the call through Axis, zero instrumentation) is on the roadmap.

Quickstart

  1. Create a project. Sign up and the dashboard walks you through it — a project scopes everything: your spans, endpoints, definitions, and API keys.
  2. Get an API key. In the dashboard, open API Keys in the sidebar and create a key with the Send spans permission (project owners and admins can do this). Keys look like trk_... and the full key is shown exactly once — copy it then.
  3. Send a span.
    Your first span (curl)
    export TRACER_API_KEY="trk_..."   # see "API keys" below
    
    curl -X POST https://ingest.axisdatamap.com/v1/spans \
      -H "Authorization: Bearer $TRACER_API_KEY" \
      -H "Content-Type: application/json" \
      -d '[{
            "trace_id":   "order-8231",
            "step_name":  "charge_payment",
            "status":     "success",
            "start_time": "2026-08-02T12:00:00Z",
            "end_time":   "2026-08-02T12:00:00.420Z"
          }]'
  4. Watch it arrive. The dashboard's Executions page polls every 10 seconds — your execution appears within moments; click it for the waterfall and graph views.

API keys

Services and scripts authenticate with project-scoped API keys. A key identifies your project — spans sent with it land in that project and nowhere else, and reads return only that project's data. You never send a project ID; the key is the tenant.

Managing keys

Keys live on the dashboard's API Keys page (sidebar). Every project member can see the list; owners and admins can create, rename, change permissions, and revoke.

  • The full key is shown exactly once, right after you create it — only a hash is stored, so nobody (us included) can show it again. The list shows a masked preview such as trk_Ab3x…9fQk so you can tell keys apart. Lost a key? Create a new one and revoke the old one.
  • Pass it as a bearer token: Authorization: Bearer trk_...
  • Optional expiration (7 days to 1 year) is set at creation; an expired key stops working on its own.
  • Last used tells you whether anything still depends on a key before you revoke it. Revoking takes effect immediately; data a key already sent is unaffected.
  • Treat keys like passwords: keep them in environment variables or your secret manager, never in client-side code or repositories.

Permissions

PermissionScopeAllows
Send spansspans:writePOST /v1/spans on the ingestion host (https://ingest.axisdatamap.com). The default for new keys.
Read executions & spansspans:readGET /v1/processes, GET /v1/processes/{trace_id}, POST /v1/spans/query on the API host (https://api.axisdatamap.com).

A key can hold either permission or both; using a key for something it wasn't granted returns 403. Give each service only what it needs — an instrumented app needs Send spans; a CI check or report script needs Read executions & spans. Keys are never a dashboard login: projects, endpoints, definitions, and keys themselves are managed only through the dashboard.

Reading data with a key

With spans:read, the same query API the dashboard uses is available to your scripts. /v1/processes lists executions (filters: status, from, to, page, page_size); /v1/processes/{trace_id} returns one execution with its spans and completeness verdict.

Query executions (curl)
# Needs a key with the "Read executions & spans" permission
curl "https://api.axisdatamap.com/v1/processes?status=failure&page_size=20" \
  -H "Authorization: Bearer $TRACER_API_KEY"

curl "https://api.axisdatamap.com/v1/processes/checkout-7f3a" \
  -H "Authorization: Bearer $TRACER_API_KEY"

Ingestion API reference

One endpoint: POST /v1/spans against your ingestion host (here https://ingest.axisdatamap.com). The body is always a JSON array — a single span is an array of one. Up to 1,000 spans per request.

Span fields

FieldTypeRequiredDescription
trace_idstringyesCorrelation ID tying spans into one execution. Use a stable business ID (order ID, request ID).
step_namestringyesHuman-readable step label, e.g. charge_payment.
statusenumyessuccess | failure | pending | error. failure = business outcome; error = crash/exception.
start_timeISO-8601 datetimeyesMUST include a timezone (e.g. trailing Z). Naive timestamps are rejected.
end_timeISO-8601 datetimenoOmit for spans still in flight (pending).
duration_msintegernoComputed from start/end when omitted; send it only if you measured it yourself.
span_idstring (uuid)noGenerated server-side when absent. Supply your own to reference it from parent_span_id.
parent_span_idstringnoNests this span under another — drawn as causal edges in the graph view.
endpoint_idstringnoWhich virtual endpoint this step maps to — its ep_ id or its name, both from the Endpoints page (a name is resolved to the id in your project). Needed for completeness.
process_def_idstringnoWhich process definition applies (from the Definitions page). Enables completeness verdicts.
attributesobjectnoArbitrary JSON metadata — amounts, HTTP statuses, error messages. Filterable in the span explorer.

A fuller example

POST /v1/spans — body
[
  {
    "trace_id":       "order-8231",
    "span_id":        "b2f6d9e0-52a1-4c53-9c4d-8f6f1f2a7b10",
    "parent_span_id": null,
    "endpoint_id":    "ep_1a2b3c...",
    "process_def_id": "proc_4d5e6f...",
    "step_name":      "charge_payment",
    "status":         "success",
    "start_time":     "2026-08-02T12:00:00Z",
    "end_time":       "2026-08-02T12:00:00.420Z",
    "attributes": {
      "http.status": 200,
      "amount_cents": 4200,
      "provider": "stripe"
    }
  },
  {
    "trace_id":   "order-8231",
    "step_name":  "send_email",
    "status":     "failure",
    "start_time": "2026-08-02T12:00:01Z",
    "end_time":   "2026-08-02T12:00:04.100Z",
    "attributes": { "error": "SMTP relay rejected the message" }
  }
]

Responses

Ingestion always answers 202 Accepted for a well-formed request. Validation happens per span: bad items are rejected individually with their index and reason — the rest of the batch is unaffected.

All accepted
HTTP/1.1 202 Accepted

{ "accepted": 1, "rejected": 0, "errors": [] }
Partial rejection (item 2 invalid)
HTTP/1.1 202 Accepted

{
  "accepted": 2,
  "rejected": 1,
  "errors": [
    {
      "index": 2,
      "detail": [
        {
          "type": "enum",
          "loc": ["status"],
          "msg": "Input should be 'success', 'failure',
                  'pending' or 'error'"
        }
      ]
    }
  ]
}

Retries & idempotency

Sending telemetry over a flaky network means retrying. Attach an Idempotency-Key header (any unique string per batch) and a retried request replays the original response without re-ingesting — valid for 24 hours.

Idempotent send
curl -X POST https://ingest.axisdatamap.com/v1/spans \
  -H "Authorization: Bearer $TRACER_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 4f7c1e9a-batch-42" \
  -d '[ ... ]'

Sending from a browser

Web pages can send spans straight from visitors' browsers. Browsers only allow that cross-site call (CORS) for websites the key explicitly lists: on the dashboard's API Keys page, add your site — e.g. https://shop.example.com — under the key's Allowed websites. List each hostname you serve from (https://example.com and https://www.example.com are different websites); wildcards aren't supported.

  • A key in a web page is public — anyone can read it from the page source. Use a dedicated key with only Send spans, so the worst misuse is extra spans, never reading your data; revoke and replace it if it gets abused.
  • The allowed-websites list stops other websites from using your key in their visitors' browsers. It can't stop someone running their own script — that's what the write-only permission and revocation are for.
  • Servers, scripts and the SDK don't send an Origin header and are never affected by this list.
  • Changes to a key's websites can take up to a minute to reach browsers.
Browser fetch()
// In a page on a website listed in the key's "Allowed websites"
fetch("https://ingest.axisdatamap.com/v1/spans", {
  method: "POST",
  keepalive: true,                       // survives page navigation
  headers: {
    Authorization: "Bearer trk_...",     // a Send-spans-only key
    "Content-Type": "application/json",
    "Idempotency-Key": "order-8231-charge",
  },
  body: JSON.stringify([{
    trace_id: "order-8231",
    step_name: "charge_payment",
    endpoint_id: "charge_payment",       // endpoint name or ep_ id
    status: "success",
    start_time: new Date().toISOString(),
  }]),
});

Correlation & trace IDs

Correlation is the heart of Axis: spans that share a trace_id become one execution. Two rules of thumb:

  • Pick an ID your whole chain already knows. An order ID or payment intent ID means every service can tag its spans without coordination.
  • Pass it along. Whatever crosses your service boundaries (message payloads, headers, job arguments) should carry the trace ID so every step tags its span with the same value.

parent_span_id is optional extra structure: set it and the graph view draws true causal edges; leave it out and spans are simply ordered by time. The Python SDK handles both automatically.

Python SDK

tracer-sdk is a zero-dependency Python client for everything above: correlation IDs, timing, statuses, nesting, batching, and retries — with one hard guarantee: telemetry failures never raise into your application. (Currently distributed with the repository under sdks/python; PyPI release planned.)

Basic usage
from tracer_sdk import Tracer

client = Tracer(
    api_key="trk_...",                        # or env TRACER_API_KEY
    ingest_url="https://ingest.axisdatamap.com",               # or env TRACER_INGEST_URL
)

with client.trace("order-8231") as trace:     # trace_id = your correlation ID
    with trace.span("charge_payment") as span:
        span.set("amount_cents", 4200)        # arbitrary attributes
        charge_the_card()                     # exception -> status="error"

    with trace.span("send_email") as span:
        if not deliver_email():
            span.fail("SMTP relay rejected")  # business failure

Clean block exit records success; an exception records error (with error.type / error.message attributes) and re-raises; span.fail("reason") records a business-level failure.

Nesting = graph edges
with client.trace("order-8231") as trace:
    with trace.span("checkout"):              # parent
        with trace.span("charge_payment"):    # child: parent_span_id set
            ...
        with trace.span("reserve_stock"):     # sibling of charge_payment
            ...

# Nesting is tracked per thread / asyncio task automatically —
# the dashboard's graph view draws these as solid causal edges.
Decorator, point events, delivery control
@trace.step("reserve_stock")                  # decorator form
def reserve(sku: str) -> None:
    ...

trace.emit("webhook_received",                # point-in-time event
           attributes={"source": "stripe"})

client.flush()                                # force-send buffered spans
client.close()                                # flush + stop (also runs atexit)

# Serverless / short-lived processes: disable the background thread and
# flush explicitly at the end of each invocation.
client = Tracer(api_key="trk_...", auto_flush=False)

Because it never raises, a rejected send is quiet: if the ingestion API refuses a batch — wrong, revoked, or expired key, a key without the Send spans permission, or an exhausted quota — the SDK drops that batch and logs a warning on the tracer_sdk logger. If spans don't show up on the dashboard, check your application logs for tracer-sdk: dropping.

Completeness monitoring

The question most monitoring can't answer is "what never happened?" A dropped email step produces no error, no log line, no span — just silence. Completeness monitoring detects that silence.

  1. Create endpoints for the services in your chain (Endpoints page).
  2. Create a process definition listing the expected steps in order (Definitions page), and turn on strict mode.
  3. Tag your spans with the endpoint (its ep_ id or its name) and the definition's proc_ id:
    Span opting into completeness
    {
      "trace_id":       "order-8231",
      "step_name":      "charge_payment",
      "endpoint_id":    "ep_1a2b3c...",    <- which declared step (ep_ id or endpoint name)
      "process_def_id": "proc_4d5e6f...",  <- which process definition applies
      "status":         "success",
      "start_time":     "2026-08-02T12:00:00Z"
    }

Every execution of that process then gets a verdict on its drill-down page and graph: missing steps (declared but never ran — shown as ghost nodes), failed steps, and out-of-order steps. Strict mode is opt-in per definition: switch it off and Axis accepts any span shape as loose logging.

Errors

  • 401 — missing, invalid, revoked, or expired API key: {"detail": "invalid or missing API key"}
  • 403 — the key is valid but lacks the permission for this call (e.g. a read-only key sending spans), or a browser called from a website the key doesn't list under Allowed websites.
  • 413 — batch larger than 1,000 spans. Split it.
  • 429 — the project's credit quota is exhausted: {"detail": "credit quota exhausted — contact your administrator"}. Spans are refused until credits are added, so retrying won't help until then.
  • 422 — the request body itself isn't a JSON array of objects. (Individual invalid spans do NOT cause 422 — they come back per-item in the 202 envelope, see above.)
  • 503 — the ingestion queue is unavailable; retry with the same Idempotency-Key.

Limits & guarantees

  • Batches: up to 1,000 spans per request.
  • Timestamps must be timezone-aware ISO-8601; naive datetimes are rejected per-item.
  • Spans are immutable: a second span with the same span_id is silently ignored, which makes retries safe — but it also means a pending span is not updated by re-sending it. Prefer emitting spans when steps finish.
  • Idempotency-Key replay window: 24 hours.
  • Ingestion is buffered and asynchronous — spans appear on the dashboard within a few seconds, and a dashboard outage never blocks your processes.
Docs · Axis