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
- Create a project. Sign up and the dashboard walks you through it — a project scopes everything: your spans, endpoints, definitions, and API keys.
- 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. - 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" }]' - 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…9fQkso 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
| Permission | Scope | Allows |
|---|---|---|
| Send spans | spans:write | POST /v1/spans on the ingestion host (https://ingest.axisdatamap.com). The default for new keys. |
| Read executions & spans | spans:read | GET /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.
# 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
| Field | Type | Required | Description |
|---|---|---|---|
| trace_id | string | yes | Correlation ID tying spans into one execution. Use a stable business ID (order ID, request ID). |
| step_name | string | yes | Human-readable step label, e.g. charge_payment. |
| status | enum | yes | success | failure | pending | error. failure = business outcome; error = crash/exception. |
| start_time | ISO-8601 datetime | yes | MUST include a timezone (e.g. trailing Z). Naive timestamps are rejected. |
| end_time | ISO-8601 datetime | no | Omit for spans still in flight (pending). |
| duration_ms | integer | no | Computed from start/end when omitted; send it only if you measured it yourself. |
| span_id | string (uuid) | no | Generated server-side when absent. Supply your own to reference it from parent_span_id. |
| parent_span_id | string | no | Nests this span under another — drawn as causal edges in the graph view. |
| endpoint_id | string | no | Which 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_id | string | no | Which process definition applies (from the Definitions page). Enables completeness verdicts. |
| attributes | object | no | Arbitrary JSON metadata — amounts, HTTP statuses, error messages. Filterable in the span explorer. |
A fuller example
[
{
"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.
HTTP/1.1 202 Accepted
{ "accepted": 1, "rejected": 0, "errors": [] }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.
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
Originheader and are never affected by this list. - Changes to a key's websites can take up to a minute to reach browsers.
// 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.)
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 failureClean 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.
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.@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.
- Create endpoints for the services in your chain (Endpoints page).
- Create a process definition listing the expected steps in order (Definitions page), and turn on strict mode.
- Tag your spans with the endpoint (its
ep_id or its name) and the definition'sproc_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 sameIdempotency-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_idis silently ignored, which makes retries safe — but it also means apendingspan 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.