The lct.me API
Share a location as a link that expires.
Create a link to a position — a fixed one, or a live session a device keeps updating — and share the URL. The link stops working when it expires or when it is revoked.
Everything under /v1 is a promise and will not break under you. Everything under /api is the browser app's own surface: undocumented on purpose, and free to move.
Getting a key
Keys are issued by hand while the API is in its first release, and there is no way to request one yet — not a signup form and not an address. Read what Professional is for where that stands.
Authentication
Your key, as x-lctme-api-key: lctme_sk_….
Not Authorization: Bearer, and that is a limitation rather than a preference. The API is served through CloudFront with origin access control, which signs each request to the origin and OVERWRITES the Authorization header with its own signature. A key sent conventionally would be discarded in transit and every call would read as unauthenticated. The conventional form needs a different front door, which is planned; when it arrives both headers will be accepted for long enough that nothing has to change on a schedule we set.
curl https://lct.me/v1/me \
-H 'x-lctme-api-key: lctme_sk_…'Endpoints
GET /v1/me
What this key is.
The smallest call that proves a key works. Worth making first from a new integration: the alternative is finding out from whichever endpoint you actually wanted, and guessing at the failure.
Responses
200 |
The key is live.
| |||||||||
401 |
No key, or a key that is not valid. The two are distinguished (no_credentials and invalid_key) because the difference is about YOUR request rather than about our data — neither answer tells an attacker whether a guessed key exists. → Error | |||||||||
403 |
The key was revoked. Codes: key_revoked. → Error | |||||||||
404 |
This deployment does not serve the business API. Codes: not_found. A 404 rather than a 500, because it is true and reveals nothing about why. → Error | |||||||||
405 |
Codes: method_not_allowed. → Error |
POST /v1/links
Create a link.
A snapshot is one fixed position. A live link is a session a device keeps updating with the returned writeToken, until sessionEndsAt or until it is revoked.
Authentication is checked BEFORE the body is read, so a bad key costs you a 401 whatever you sent — a malformed payload will not tell you that it was otherwise acceptable.
Request body
| Field | Type | |
|---|---|---|
position required |
Position |
|
kind |
"snapshot" | "live" (default "snapshot") |
An unrecognised value is refused rather than defaulted: a client asking for a kind this server does not have will misread whatever it gets back. |
lifetimeSeconds |
number |
Snapshots. Capped by the deployment's maximum. |
sessionSeconds |
number |
Live links. Capped by the business maximum, which is higher than the one anonymous browser creates get. |
Responses
201 |
Created. writeToken is present for live links and will never appear again. → Link |
400 |
Codes: invalid_json, invalid_body, invalid_position, invalid_kind, invalid_lifetime, invalid_session. → Error |
401 |
No key, or a key that is not valid. The two are distinguished (no_credentials and invalid_key) because the difference is about YOUR request rather than about our data — neither answer tells an attacker whether a guessed key exists. → Error |
403 |
The key was revoked. Codes: key_revoked. → Error |
404 |
This deployment does not serve the business API. Codes: not_found. A 404 rather than a 500, because it is true and reveals nothing about why. → Error |
405 |
Codes: method_not_allowed. → Error |
GET /v1/usage
What you have used.
Defaults to the current UTC calendar month, which is the period an invoice covers.
Every boundary is UTC. A day that starts when a server happens to think it does is a total you cannot reproduce.
There is no parameter naming a customer, and that is structural rather than a check we remembered to write: the key you authenticate with is the only one you can read.
Query parameters
| Name | Type | |
|---|---|---|
from |
string (date) |
YYYY-MM-DD, inclusive. A date that does not exist is refused. |
to |
string (date) |
YYYY-MM-DD, inclusive. |
Responses
200 |
Totals for the period, and a line per day — reconciling an invoice means finding the day that surprised you, and one number gives you nowhere to look.
| ||||||||||||||||||
400 |
Codes: invalid_period. → Error | ||||||||||||||||||
401 |
No key, or a key that is not valid. The two are distinguished (no_credentials and invalid_key) because the difference is about YOUR request rather than about our data — neither answer tells an attacker whether a guessed key exists. → Error | ||||||||||||||||||
403 |
The key was revoked. Codes: key_revoked. → Error | ||||||||||||||||||
404 |
This deployment does not serve the business API. Codes: not_found. A 404 rather than a 500, because it is true and reveals nothing about why. → Error | ||||||||||||||||||
405 |
Codes: method_not_allowed. → Error |
GET /v1/openapi.json
This document.
Responses
200 |
The OpenAPI document. |
405 |
Codes: method_not_allowed. → Error |
Objects
Position
Optional fields are omitted rather than sent as null. A device that cannot measure altitude should leave the key out; null is refused so that 'unknown' has one representation instead of two.
| Field | Type | |
|---|---|---|
latitude required |
number (-90 to 90) |
|
longitude required |
number (-180 to 180) |
|
accuracy required |
number (0 or more) |
Radius in metres, as the browser's Geolocation API reports it. |
altitude |
number |
Metres above the WGS 84 ellipsoid. |
altitudeAccuracy |
number |
|
heading |
number |
Degrees clockwise from true north. |
speed |
number |
Metres per second. |
Link
| Field | Type | |
|---|---|---|
id required |
string |
The path segment of the shareable URL. |
kind required |
"snapshot" | "live" |
|
position |
object |
Optional fields are omitted rather than sent as null. A device that cannot measure altitude should leave the key out; null is refused so that 'unknown' has one representation instead of two. |
createdAt required |
string (date-time) |
|
expiresAt required |
string (date-time) |
After this the link returns 404. Expiry is checked on every read. |
positionAt |
string (date-time) |
Live links only. When the position was last updated — a viewer needs it to tell a moving dot from one that stopped reporting, which look identical without it. |
sessionEndsAt |
string (date-time) |
Live links only. |
name |
string |
Group links only, and optional: what the organiser is called on their own roster. Validated by the same rule a joiner's name is, because they end up in one list. Refused on a link that is not a group — a name with no roster to show it on would be collected and discarded. |
join |
boolean |
Present and true only on a live link other people may share into (a group). The creation response also carries a one-time joinToken. NOTE: the same gap as trail — joining and reading the members is /api only, which is not part of this contract, so a link created with join here cannot be completed through /v1. Do not depend on this field until that is closed. |
members |
object[] |
Present only on a group link. The first entry is the link's own position, marked self; the rest are people who joined. One uniform list on purpose — the organiser is stored on the link record and the joiners as separate items, and a client drawing the group should not have to know that. |
trail |
boolean |
Present and true only on a live link that is keeping its path. Off unless asked for at creation, and it cannot be turned on for a session already running — the person sharing agreed to what the link was when they started it. NOTE: the route itself is not readable through this contract yet, only through /api, which is not part of it. Creating a link with trail here and having no documented way to read it back is a gap and is being closed; until it is, do not depend on this field. |
session |
"pending" | "open" | "ended" | "revoked" |
Live links only. pending means the link exists but carries no position yet — a request nobody has answered. It is reachable through /v1 only by reading a link created elsewhere; there is no business endpoint that creates one (see /api, which is not part of this contract). |
revokedAt |
string (date-time) |
Present only on a revoked live link, which carries no position at all: revoking DELETES the coordinates from the record rather than withholding them here. |
url |
string |
The shareable URL, so there is nothing to assemble. Present only when the deployment has been told its own origin. |
writeToken |
string |
Live creates only, and RETURNED EXACTLY ONCE — it is never in any later read of the link, because those go to viewers. Whoever holds it can update the position. Store it when you get it; there is no way to ask for it again. |
Error
| Field | Type | |
|---|---|---|
error required |
object |
Errors
Every failure is the same shape. Branch on error.code, which is
stable; error.message is written for a person and may be
reworded.
{ "error": { "code": "invalid_position", "message": "latitude must be between -90 and 90." } }What is not here yet
Named honestly rather than left to be discovered: webhooks and custom domains are not built. Both are deferred until a customer defines what they need, because publishing an event taxonomy invites integrations against it — and then it is not a guess any more, it is a commitment.
Two things that are built and worth knowing about: usage is counted
per key per UTC day and readable at /v1/usage before any invoice
quotes it, and the key travels in a custom header rather than
Authorization for the reason given above.