txmesh
New to mesh? Radios Reference ← Home
Developers · Pathbot API

Pathbot API

Hand a MeshCore !path hop chain to txmesh and get back a short, shareable map link — no resolver, storage or map page of your own. Read it back as JSON or GeoJSON whenever you want to draw it yourself.

Quick start

Base URL https://txme.sh. Everything below is server-to-server / device-to-server: submit a hop chain, get a URL back, paste it into a channel.

curl -X POST https://txme.sh/api/device/path \
  -H 'content-type: application/json' \
  -H 'x-meshcore-pubkey: 4b19c0de7f1a2b3c' \
  -d '{"h":"90e2,fe27","o":"30.267153,-97.743061","n":"TX-Hill Repeater","r":"kk5abc"}'

{"url":"https://txme.sh/p/RiYpPco","resolved":1,"km":41.8}

That url is a hosted map page showing every hop the packet took, which repeaters they resolved to, and how far the whole path travelled. It stays live and improves on its own as txmesh learns about more repeaters.

Machine-readable spec

Everything on this page is also an OpenAPI 3.1 document covering exactly this public surface — import it into Postman or Insomnia, point a code generator at it, or read it as a rendered reference:

WhatWhere
/docs/api/openapi.yamlThe authored contract, application/yaml
/docs/api/openapi.jsonThe same document as JSON, for tools that won't parse YAML
/docs/api/referenceRendered reference — every schema, field and example
curl -sO https://txme.sh/docs/api/openapi.yaml

The two spec files are the only routes here that send Access-Control-Allow-Origin: *, so browser tooling can load them by URL. That is the document, not the API — the endpoints it describes stay CORS-less.

This document is written by hand and describes only what a third party can call. It is not generated from the application: the framework's own /docs, /redoc and /openapi.json are disabled, because they described every account-only route as well.

Authentication

Only submission needs an identity; reading a path is public. Every submission is attributed to a sender, and txmesh picks that sender from the request in this order — the first one that matches wins:

OrderHeaderSender identityCoordinates stored
1x-device-tokenA token label we issued youFull precision
2x-meshcore-pubkeyYour radio's public keyCoarsened to ~1 km, until an admin trusts you
3noneYour IP address — open on txmesh, off by default elsewhereCoarsened to ~1 km

A wrong x-device-token is not an error — evaluation just falls through to the next branch — so a typo'd token shows up as a silent downgrade to coarsened coordinates, never as "invalid token". On a deployment where anonymous submission is off, a request with no usable identity gets 403.

txmesh accepts anonymous submissions, so a bare POST with no headers at all will work here. Send your public key anyway: it gives you your own rate-limit budget instead of one shared with everyone behind your address, keeps your reports grouped as one identity, and is the only route to storing precise coordinates.

Using your radio's public key

This is the path of least resistance and needs nothing from us: send your node's MeshCore public key in a header on every submission.

x-meshcore-pubkey: 4b19c0de7f1a2b3c9d8e...
A public key is membership, not authentication. MeshCore public keys are broadcast over the air — they are not secret, and this header is unauthenticated, so any caller can assert any node's key. That is a deliberate, accepted tradeoff to keep submission open. Treat it as attribution, never as a credential, and never assume a path attributed to a key was really submitted by that radio. Because the header is cheap to rotate, pubkey senders are additionally capped per submitting IP address.

Static tokens

If you run a bot or a firmware fleet and want your reported coordinates stored at full precision from the first request, ask this deployment's operator for a device token ([email protected]) and send it as x-device-token. We issue it from the admin panel and can revoke it the same way; we only ever see it once, so keep your copy. It is a real shared secret, compared in constant time, and it is the only identity here that is one. Token senders are pre-trusted and are exempt from the per-IP ceiling, since a fleet legitimately shares one egress address.

Getting trusted with a pubkey

Untrusted senders' endpoint coordinates (the o and q fields) are rounded to two decimals — roughly a 1 km grid — before they are written. That is irreversible: the precise value is never stored. Submit a few paths, then email us your 8-hex key and an admin can mark your sender trusted, after which your submissions store precisely. Per-hop repeater positions come from the public roster and are never coarsened either way.

Reading needs nothing

All three GET /api/path/… endpoints and the /p/{slug} page are open to anonymous callers — that is the point of a link you can paste into a LoRa channel. Signing in with a txmesh account (a browser cookie; there is no bearer token for reads) only changes one thing: it suppresses the read-time coarsening of the two endpoint coordinates.

Submitting a path

POST /api/device/path

The wire format uses single-letter keys on purpose: the reference sender is a microcontroller building JSON with snprintf into a 256-byte buffer. Content type may be application/json or absent. Unknown keys are ignored.

FieldRequiredMeaning
hyesThe hop-hash chain, comma-joined, in traversal order (requester side first, reporting node last). Each hash is the leading 1–3 bytes of a repeater's public key as hex — exactly 2, 4 or 6 characters — and all hashes must be the same width, since hash size is uniform within one MeshCore packet. 1 to 64 hops. Example: 90e2,fe27.
onoThe reporter's position (the node that answered the !path) as "lat,lon" in decimal degrees. The chain's end anchor. 0,0 is rejected as MeshCore's "position not set" sentinel.
qnoThe requester's position (the node that ran !path), same format — typically from that node's advert/wardrive beacon. The chain's start anchor.
nnoReporter node's display name. Stored truncated to 40 characters.
rnoRequester node's display name. Stored truncated to 40 characters.
snoPer-hop SNR in dB, comma-joined, traversal order — exactly one value per hop, each within ±40. Example: 11.5,-3.25.

Only h can fail the request. A malformed or out-of-range o, q or s is silently dropped: the anchor simply becomes null, and an s list with the wrong number of values is discarded wholesale. A 200 does not mean every field you sent was accepted.

Response

{"url":"https://txme.sh/p/RiYpPco","resolved":1,"km":41.8}

url is the map page (see short links), resolved is how many hops matched a known node, and km is the total path length — omitted entirely, not null, when nothing could be measured. The "url":"…" shape is a compatibility guarantee: deployed firmware finds it with strstr, not a JSON parser, so it will never be renamed or nested.

Short links

By default url is the full /p/{slug} link. An admin can tick short links for your sender in the admin panel, which runs your links through da.gd — worth roughly six characters on a ~160-character LoRa frame. It is off for every new sender, because shortening hands the trace URL to a third party. Email us if you want it on.

Shortening is best-effort: a 2-second cap, and any failure falls back to the full URL. So url is always a working link, never guaranteed to be a short one — read it from the response rather than assuming a form.

Errors

StatusBodyCause
400{"error":"bad hashes"}h was non-hex, empty, over 64 hops, wider than 3 bytes, or mixed widths.
403{"error":"submission requires a device token or a mesh pubkey"}No sender identity — see Authentication.
422{"detail":[…]}Body failed validation: missing h, a field over its length cap, malformed JSON, or an unsupported content type.
429{"error":"rate limited"}Over a per-minute cap. Carries Retry-After: 60.

Note the two envelopes: the pathbot routes return {"error": …}, while framework-level validation returns a detail array. Checks run in this order — body validation, then identity, then rate limit, then hash validation — so an unauthenticated caller with a bad body sees 422, not 403.

Retries are safe

A logically identical resubmission — same hashes, same coordinates, same names — within 15 minutes reuses the existing record: you get the same slug back, no new row is created, and no rate-limit budget is spent. Firmware re-asks when a LoRa reply is lost, so treat this endpoint as idempotent-within-15-minutes rather than building your own dedupe, and never assume a fresh slug per call.

Reading a path

Public while the deployment leaves public path viewing on (the default here). When it is off, anonymous callers get 401 {"error":"authentication required"} before the slug is even looked up, so these endpoints can't be used to fish for valid slugs. An unknown or expired slug is 404 {"error":"not found"}. None of the reads are rate limited.

GET /api/path/{slug}

The richest view, and exactly what the /p/ page itself fetches: every hop with its resolved node, coordinates and per-hop distance, alternate candidates where a hash was ambiguous, the two endpoint anchors, SNRs, and path-length totals. Coordinates here are {"lat": …, "lon": …}. Anonymous responses carry "coordsRounded": true when read-time coarsening was applied.

GET /api/path/{slug}/geojson

The same path as an RFC 7946 FeatureCollection, served as application/geo+json — one Point per positioned hop, one per known endpoint, and a LineString tracing requester → hops → reporter. Drops straight into Leaflet, MapLibre, geojson.io or an existing GeoJSON pipeline. Identifiers (slug, page URL, names, counts, totalKm) ride along in a metadata member, so you need no second request.

Coordinate order differs between the two views. GeoJSON is [longitude, latitude]; the JSON view uses named lat/lon keys. Unresolved hops have no position, so both the points and the line skip them — a hop feature's seq is its index into the full hop list and legitimately has gaps.
GET /api/path/{slug}/related

Other reports by the same requester — the "more from this node" rail. Grouped by public key when the requester is in the roster (so a node that renamed itself still groups as one identity), falling back to exact name when it isn't; byPubkey tells you which happened. Results are also scoped to the submitting sender of the reference slug, so you only ever see reports that arrived through the same submitter. Deliberately returns no coordinates — slugs and coarse statistics only, newest first, capped at 100 with no paging.

GET /p/{slug}

The human-facing map page. The slug is not validated server-side, so an unknown slug still returns a 200 shell that then renders "Path not found" — use GET /api/path/{slug} for existence checks, never this.

Slugs

Seven characters from the URL-safe base64 alphabet, minted at submit time. They are meant to be pasted into LoRa channels: unguessable-ish, but not a capability and not a secret.

Rate limits

The read endpoints are not rate limited by the application, but they carry no Cache-Control either — poll politely.

Things that will bite you

Questions, a device token, or a trusted-sender request: [email protected].
Community-run MeshCore bot framework · Texas How weather & flood alerts work ↗ Shared !path links are public, no account needed.