/documentation.txt · /api/v2/openapi.json
Fernscout dev — v2 API
Generated from the frozen Zod contract in lib/api/v2/schemas/ — every field, enum and required/optional marker here is what a validator actually checks, not a second hand-typed copy of it. Every path below is relative to https://dev.fernscout.ch, and every "{user}" is a journal's username (e.g. "example").
/api/v2/openapi.json
getThis document. No auth.
Responses
- 200
the v2 OpenAPI 3.1 document
- 405
a verb this route does not answer
/api/v2/status
getWhat this instance can do, and what things cost. No auth.
Responses
- 200
the instance's capabilities, limits and pricing
- 405
a verb this route does not answer
/api/v2/journals/available
getWhether a username is free to sign up with — send ?username=.
Responses
- 200
always 200 — availability is reported in the body, not the status
- 400
invalid_request— no ?username= given
/api/v2/journals
postCreate a journal, spending a signup token minted by POST /api/auth/codes.
Request body
application/json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"username": {
"type": "string",
"minLength": 1
},
"title": {
"type": "string",
"minLength": 1,
"maxLength": 200
},
"ownerName": {
"type": "string",
"minLength": 1
},
"ownerNickname": {
"type": "string",
"minLength": 1
},
"tagline": {
"type": "string",
"minLength": 1
},
"visibility": {
"type": "string"
},
"defaultLocale": {
"type": "string"
},
"locales": {
"type": "array",
"items": {
"type": "string"
}
},
"baseCurrency": {
"type": "string"
},
"displayCurrencies": {
"type": "array",
"items": {
"type": "string"
}
},
"units": {
"type": "string",
"enum": [
"metric",
"imperial"
]
},
"tips": {
"type": "boolean"
}
},
"required": [
"username",
"title",
"ownerName",
"ownerNickname"
],
"additionalProperties": false
}Responses
- 201
the journal exists; a one-time sign-in link and the agent token ride along
- 400
invalid_request;phone_required;invalid_username;invalid_title;invalid_owner- 401
missing_token;invalid_token- 403
signup_not_invited— this instance is invite-only and the address is not on its list;reserved_username;too_many_journals- 404
signup_disabled- 409
username_taken;tel_taken- 410
deleted_username- 413
body_too_large— the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status- 429
too_many_requests— per-IP creation/refusal rate limit
/api/v2/me/home
getWhat this address may open, and its own device list. No bearer token — an fs_identity cookie, or nobody.
Responses
- 200
always 200 —
id: nullis a stranger, not a refusal- 405
a verb this route does not answer
/api/v2/me/devices
deleteSign out everywhere: end every browser sign-in this address holds — each device's identity and each journal's reader cookie. Agent keys and GPS tokens are not touched. Browser cookie only.
Responses
- 200
how many sign-ins ended; this browser's cookies are cleared too
- 401
not_signed_in- 403
foreign_origin- 404
auth_disabled
/api/v2/me/devices/{id}
deleteEnd one device's identity. The id is checked against this address's own list.
Responses
- 200
revoked
- 401
not_signed_in- 404
auth_disabled;no_such_device
/api/v2/geocode
postA place name into candidate coordinates, never a guess.
Request body
application/json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"query": {
"type": "string",
"minLength": 1
},
"countryHint": {
"type": "string"
},
"regionHint": {
"type": "string"
},
"contextCoordinates": {
"type": "array",
"items": {
"type": "object",
"properties": {
"lat": {
"type": "number",
"minimum": -90,
"maximum": 90
},
"lng": {
"type": "number",
"minimum": -180,
"maximum": 180
}
},
"required": [
"lat",
"lng"
],
"additionalProperties": false
}
}
},
"required": [
"query"
],
"additionalProperties": false
}Responses
- 200
a ranked shortlist, possibly empty
- 400
invalid_request- 401
missing_token;invalid_token- 404
address_lookup_disabled- 413
body_too_large— the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status- 429
too_many_requests- 502
provider_unavailable
/api/v2/{user}
getThe journal document.
Responses
- 200
the stored journal, with its ETag
- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
no_such_journal
patchMerge-patch the journal document. A CHANGED owner.email does not write — it starts a verification at `.../owner/email/redeem` instead, and answers 202.
Request body
application/json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"title": {
"type": "string",
"minLength": 1,
"maxLength": 200
},
"owner": {
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1
},
"nickname": {
"type": "string",
"minLength": 1
},
"email": {
"type": "string",
"format": "email",
"pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
}
},
"required": [
"name",
"nickname",
"email"
],
"additionalProperties": false
},
"locales": {
"minItems": 1,
"type": "array",
"items": {
"type": "string"
}
},
"baseCurrency": {
"type": "string",
"minLength": 3,
"maxLength": 3
},
"displayCurrencies": {
"minItems": 1,
"type": "array",
"items": {
"type": "string",
"minLength": 3,
"maxLength": 3
}
},
"units": {
"type": "string",
"enum": [
"metric",
"imperial"
]
},
"visibility": {
"type": "string",
"enum": [
"public",
"guest"
]
},
"tagline": {
"type": "string"
},
"figures": {
"anyOf": [
{
"type": "object",
"properties": {
"mode": {
"type": "string",
"const": "off"
}
},
"required": [
"mode"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"mode": {
"type": "string",
"const": "set"
},
"figures": {
"minItems": 1,
"maxItems": 10,
"type": "array",
"items": {
"type": "string",
"minLength": 1
}
}
},
"required": [
"mode",
"figures"
],
"additionalProperties": false
}
]
},
"declined": {
"type": "object",
"propertyNames": {
"type": "string",
"enum": [
"tagline",
"figures"
]
},
"additionalProperties": {
"type": "string",
"minLength": 10
}
}
},
"additionalProperties": false
}Responses
- 200
the merged, re-validated document
- 202
owner.email changed to a different, valid address — a code went to it, and nothing was written
- 400
invalid_request- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
no_such_journal- 409
stale_document— If-Match did not cover the current ETag- 413
body_too_large— the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status- 422
incomplete— the merged document leaves a required section neither answered nor declined- 429
too_many_requests— too many owner-email codes for this address or this journal today- 503
mail_disabled— owner.email changed, but this server cannot send the verification code;mail_failed— owner.email changed, but the verification code could not be sent
deleteAsk to delete the journal — mails the owner a single-use confirmation link. Deletes nothing itself.
Responses
- 202
a mail is on its way; nothing has been deleted
- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
no_such_journal- 409
mail_disabled- 410
gone— already deleted- 429
too_many_requests— too many deletion mails to this address recently
/api/v2/{user}/status
getWhere this journal and this token stand: drafts, trips, storage, inbox.
Responses
- 200
the journal's live status
- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip- 404
no_such_journal
/api/v2/{user}/storage
getWhere this journal's storage is going.
Responses
- 200
usage, breakdown, what could be reclaimed, and the price of more
- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip- 404
no_such_journal
/api/v2/{user}/sync/manifest
getEvery file this journal's folder holds, with a hash — the up-to-date check for a local mirror, and a complete one: the full-resolution originals are included, so a first pull is large and a later one carries only what the hashes say changed. This journal's position history is in no manifest and behind no route. Owner only.
Responses
- 200
path, size and hash of every syncable file
- 401
missing_token;invalid_token- 404
not_found— the same refusal whether the journal is unknown, the token is for a different one, or it is merely trip-scoped
/api/v2/{user}/sync/file/{path}
getOne file's bytes, by the path the manifest named. Read-only, owner only.
Responses
- 200
the file's bytes, with a content-type guessed from its name
- 401
missing_token;invalid_token- 404
not_found— the same refusal whether the journal is unknown, the token is for a different one, or it is merely trip-scoped
/api/v2/{user}/import
getThe kinds and formats this door reads, and the ceiling on a request's size. Owner only.
Responses
- 200
kinds, formats and the byte ceiling
- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
no_such_journal
postImport a location history (`kind: "gps"`, stored as read) or a phone's address book (`kind: "contacts"`, read and reported — nothing is written until the agreed rows are sent to POST .../contacts/import). Send ?dryRun to preview. JSON `{kind, format?, inbox|text}`, or multipart with `file`. Owner only, with one exception (B2204): a `write:gps` token — minted at POST /api/auth/{user}/gps-token — may call this too, and only this, and only for `kind: "gps"` with `dryRun` false or absent; its response has no `extent`. Everything else about this door refuses that token exactly as it refuses any other non-owner call.
Request body
application/json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"kind": {
"type": "string",
"enum": [
"gps",
"contacts"
]
},
"format": {
"type": "string"
},
"inbox": {
"type": "string"
},
"text": {
"type": "string"
}
},
"required": [
"kind"
],
"additionalProperties": false
}Responses
- 200
a gps import, stored (or previewed under ?dryRun) — or a contacts import, read and reported, never written
- 400
invalid_request;expected_file;invalid_body;no_file;unknown_kind;storage_full;unreadable;contract- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
no_such_journal;unknown_user;unknown_inbox_file- 413
body_too_large
/api/v2/{user}/gps
getWhich months of raw location history this journal holds — names, never a fix. Owner only.
Responses
- 200
YYYY-MM, sorted
- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
no_such_journal
/api/v2/{user}/channels
getThe owner's own mute switches for the sending channels.
Responses
- 200
null for a channel this instance does not offer at all
- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
no_such_journal
patchSwitch mail and/or WhatsApp sending on or off for this journal.
Request body
application/json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"mail": {
"type": "boolean"
},
"whatsapp": {
"type": "boolean"
}
},
"additionalProperties": false
}Responses
- 200
the channels as they now stand
- 400
invalid_request- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
no_such_journal- 409
capability_unavailable- 413
body_too_large— the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status
/api/v2/{user}/owner/tel
getThe owner's own telephone number — a notification channel, not the address that owns the journal.
Responses
- 200
null fields when there is no number on file
- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
no_such_journal
deleteClear the owner's own number — turns their free WhatsApp copy of a day back off.
Responses
- 200
tel: null
- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
no_such_journal
/api/v2/{user}/owner/tel/verify
postStart proving a number for the owner's own telephone field — sends a one-time code.
Request body
application/json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"tel": {
"type": "string",
"minLength": 1
}
},
"required": [
"tel"
],
"additionalProperties": false
}Responses
- 202
an opaque id — bring it, with the code, to
.../verify/redeem- 400
invalid_request- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
no_such_journal- 409
capability_unavailable- 413
body_too_large— the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status- 429
too_many_requests- 503
verification_failed
/api/v2/{user}/owner/tel/verify/redeem
postFinish proving a number — the code from `.../verify` writes owner.tel, proven, for good.
Request body
application/json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"id": {
"type": "string",
"minLength": 1
},
"code": {
"type": "string",
"minLength": 1
}
},
"required": [
"id",
"code"
],
"additionalProperties": false
}Responses
- 200
the number, now proven
- 400
invalid_request- 401
missing_token;invalid_token;invalid_code- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
no_such_journal- 413
body_too_large— the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status- 429
too_many_requests
/api/v2/{user}/owner/email/redeem
postFinish moving owner.email — the code the new address received writes it, revokes every session and agent token the old address held for this journal, and mails the old address.
Request body
application/json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"id": {
"type": "string",
"minLength": 1
},
"code": {
"type": "string",
"minLength": 1
}
},
"required": [
"id",
"code"
],
"additionalProperties": false
}Responses
- 200
the journal document, with owner.email now moved
- 400
invalid_request- 401
missing_token;invalid_token;invalid_code- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
no_such_journal- 413
body_too_large— the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status- 429
too_many_requests
/api/v2/{user}/trips
getEvery trip this token may write, paged.
Responses
- 200
one page of trip documents
- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip- 404
no_such_journal
/api/v2/{user}/trips/{trip}
getOne trip. `?days=full|summaries|none` controls how much of each day rides along (default full, dayDoc[]); summaries and none change the shape of `days` away from what `tripDoc` documents for a plain GET.
Responses
- 200
the trip, with its days inline (mode
full)- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip- 404
no_such_journal;unknown_trip
patchMerge-patch a trip.
Request body
application/json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"id": {
"type": "string",
"pattern": "^[a-z0-9][a-z0-9-]*$"
},
"title": {
"type": "string",
"minLength": 1,
"maxLength": 200
},
"dates": {
"type": "object",
"properties": {
"from": {
"type": "string",
"pattern": "^\\d{4}-\\d{2}-\\d{2}$"
},
"to": {
"type": "string",
"pattern": "^\\d{4}-\\d{2}-\\d{2}$"
}
},
"required": [
"from",
"to"
],
"additionalProperties": false
},
"visibility": {
"type": "string",
"enum": [
"private",
"public",
"guest"
]
},
"people": {
"minItems": 1,
"maxItems": 10,
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1
},
"nickname": {
"type": "string",
"minLength": 1
},
"email": {
"type": "string",
"format": "email",
"pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
}
},
"required": [
"name",
"email"
],
"additionalProperties": false
}
},
"teaser": {
"type": "boolean"
},
"rates": {
"type": "object",
"properties": {
"currencies": {
"minItems": 1,
"type": "array",
"items": {
"type": "string",
"minLength": 3,
"maxLength": 3
}
},
"manual": {
"type": "object",
"propertyNames": {
"type": "string",
"minLength": 3,
"maxLength": 3
},
"additionalProperties": {
"type": "number",
"exclusiveMinimum": 0
}
}
},
"required": [
"currencies"
],
"additionalProperties": false
},
"costs": {
"type": "object",
"properties": {
"budget": {
"type": "object",
"properties": {
"total": {
"type": "number",
"exclusiveMinimum": 0
},
"days": {
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991
},
"currency": {
"type": "string",
"minLength": 3,
"maxLength": 3
}
},
"required": [
"total"
],
"additionalProperties": false
},
"items": {
"maxItems": 200,
"type": "array",
"items": {
"type": "object",
"properties": {
"label": {
"type": "string",
"minLength": 1,
"maxLength": 200
},
"amount": {
"type": "number",
"exclusiveMinimum": 0
},
"category": {
"type": "string",
"enum": [
"preparation",
"flights",
"accommodation",
"food",
"transport",
"activities",
"other"
]
},
"currency": {
"type": "string",
"minLength": 3,
"maxLength": 3
},
"stop": {
"type": "string"
}
},
"required": [
"label",
"amount"
],
"additionalProperties": false
}
},
"note": {
"type": "string"
},
"visibility": {
"type": "string",
"enum": [
"public",
"guests"
]
}
},
"additionalProperties": false
},
"plan": {
"type": "object",
"properties": {
"route": {
"minItems": 1,
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"pattern": "^[a-z0-9][a-z0-9-]*$"
},
"location": {
"type": "string",
"minLength": 1
},
"lat": {
"type": "number",
"minimum": -90,
"maximum": 90
},
"lng": {
"type": "number",
"minimum": -180,
"maximum": 180
},
"country": {
"type": "string"
},
"countryCode": {
"type": "string",
"minLength": 2,
"maxLength": 2
},
"note": {
"type": "string"
},
"nights": {
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991
},
"arrive": {
"type": "string",
"pattern": "^\\d{4}-\\d{2}-\\d{2}$"
},
"leave": {
"type": "string",
"pattern": "^\\d{4}-\\d{2}-\\d{2}$"
},
"see": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1
},
"lat": {
"type": "number",
"minimum": -90,
"maximum": 90
},
"lng": {
"type": "number",
"minimum": -180,
"maximum": 180
},
"source": {
"type": "string",
"enum": [
"typed",
"maps-link",
"pin",
"coordinates"
]
}
},
"required": [
"name",
"lat",
"lng"
],
"additionalProperties": false
}
},
"source": {
"type": "string",
"enum": [
"typed",
"maps-link",
"pin",
"coordinates"
]
}
},
"required": [
"location",
"lat",
"lng"
],
"additionalProperties": false
}
},
"body": {
"type": "string"
},
"mode": {
"type": "string",
"enum": [
"nights",
"dates"
]
},
"readers": {
"type": "string",
"enum": [
"map",
"details"
]
},
"private": {
"type": "object",
"properties": {
"links": {
"type": "array",
"items": {
"type": "object",
"properties": {
"label": {
"type": "string",
"minLength": 1
},
"url": {
"type": "string",
"format": "uri"
}
},
"required": [
"label",
"url"
],
"additionalProperties": false
}
},
"stops": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "object",
"properties": {
"stay": {
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1
},
"lat": {
"type": "number",
"minimum": -90,
"maximum": 90
},
"lng": {
"type": "number",
"minimum": -180,
"maximum": 180
}
},
"required": [
"name",
"lat",
"lng"
],
"additionalProperties": false
},
"links": {
"type": "array",
"items": {
"type": "object",
"properties": {
"label": {
"type": "string",
"minLength": 1
},
"url": {
"type": "string",
"format": "uri"
}
},
"required": [
"label",
"url"
],
"additionalProperties": false
}
}
},
"additionalProperties": false
}
}
},
"additionalProperties": false
}
},
"required": [
"route"
],
"additionalProperties": false
},
"days": {
"type": "array",
"items": {
"type": "object",
"properties": {
"slug": {
"type": "string",
"pattern": "^\\d{4}-\\d{2}-\\d{2}-[a-z0-9]+(-[a-z0-9]+)*$"
},
"title": {
"type": "string",
"minLength": 1,
"maxLength": 200
},
"date": {
"type": "string",
"pattern": "^\\d{4}-\\d{2}-\\d{2}$"
},
"content": {
"type": "string",
"maxLength": 100000
},
"media": {
"type": "array",
"items": {
"type": "object",
"properties": {
"src": {
"type": "string"
},
"caption": {
"type": "string"
},
"visibility": {
"type": "string",
"enum": [
"guest",
"private"
]
}
},
"required": [
"src"
],
"additionalProperties": false
}
},
"costs": {
"maxItems": 200,
"type": "array",
"items": {
"type": "object",
"properties": {
"label": {
"type": "string",
"minLength": 1,
"maxLength": 200
},
"amount": {
"type": "number",
"exclusiveMinimum": 0
},
"category": {
"type": "string",
"enum": [
"preparation",
"flights",
"accommodation",
"food",
"transport",
"activities",
"other"
]
},
"currency": {
"type": "string",
"minLength": 3,
"maxLength": 3
},
"stop": {
"type": "string"
}
},
"required": [
"label",
"amount"
],
"additionalProperties": false
}
},
"coordinates": {
"type": "object",
"properties": {
"lat": {
"type": "number",
"minimum": -90,
"maximum": 90
},
"lng": {
"type": "number",
"minimum": -180,
"maximum": 180
}
},
"required": [
"lat",
"lng"
],
"additionalProperties": false
},
"weather": {
"anyOf": [
{
"type": "boolean",
"const": true
},
{
"type": "object",
"properties": {
"tempMin": {
"type": "number",
"minimum": -90,
"maximum": 60
},
"tempMax": {
"type": "number",
"minimum": -90,
"maximum": 60
},
"code": {
"type": "integer",
"minimum": 0,
"maximum": 99
},
"precipitation": {
"type": "number",
"minimum": 0,
"maximum": 2000
},
"windMax": {
"type": "number",
"minimum": 0,
"maximum": 500
},
"recordedAt": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
},
"source": {
"type": "string",
"minLength": 1
}
},
"required": [
"recordedAt",
"source"
],
"additionalProperties": false
}
]
},
"time": {
"type": "string",
"pattern": "^([01]\\d|2[0-3]):[0-5]\\d$"
},
"timezone": {
"type": "string"
},
"location": {
"type": "string"
},
"country": {
"type": "string"
},
"countryCode": {
"type": "string",
"minLength": 2,
"maxLength": 2
},
"transportMode": {
"type": "string",
"enum": [
"flight",
"train",
"bus",
"motorbike",
"bicycle",
"boat",
"car",
"taxi",
"walk",
"metro",
"tram",
"ferry"
]
},
"tags": {
"maxItems": 10,
"type": "array",
"items": {
"type": "string",
"maxLength": 30,
"pattern": "^[a-z0-9]+(-[a-z0-9]+)*$"
}
},
"translations": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"content": {
"type": "string"
}
},
"required": [
"title",
"content"
],
"additionalProperties": false
}
},
"visibility": {
"type": "string",
"enum": [
"guest",
"private"
]
},
"status": {
"type": "string",
"const": "draft"
},
"declined": {
"type": "object",
"propertyNames": {
"type": "string",
"enum": [
"media",
"costs",
"coordinates",
"weather",
"time",
"timezone",
"location",
"country",
"countryCode",
"transportMode",
"tags",
"translations",
"visibility",
"status"
]
},
"additionalProperties": {
"type": "string",
"minLength": 10
}
},
"transportFrom": {
"type": "string"
},
"transportTo": {
"type": "string"
},
"travelScene": {
"type": "string",
"enum": [
"default",
"quick",
"skip"
]
},
"test": {
"type": "boolean"
}
},
"required": [
"slug",
"title",
"date",
"content"
],
"additionalProperties": false
}
},
"translations": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"tagline": {
"type": "string"
},
"intro": {
"type": "string"
}
},
"additionalProperties": false
}
},
"accent": {
"anyOf": [
{
"type": "string",
"enum": [
"sky",
"yellow",
"green",
"coral",
"navy"
]
},
{
"type": "null"
}
]
},
"cover": {
"type": [
"string",
"null"
]
},
"figures": {
"anyOf": [
{
"type": "object",
"properties": {
"mode": {
"type": "string",
"const": "off"
}
},
"required": [
"mode"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"mode": {
"type": "string",
"const": "journal"
}
},
"required": [
"mode"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"mode": {
"type": "string",
"const": "custom"
},
"figures": {
"minItems": 1,
"maxItems": 10,
"type": "array",
"items": {
"type": "string",
"minLength": 1
}
}
},
"required": [
"mode",
"figures"
],
"additionalProperties": false
}
]
},
"tagline": {
"type": [
"string",
"null"
]
},
"intro": {
"type": [
"string",
"null"
]
},
"listed": {
"type": "boolean"
},
"reminder": {
"type": "object",
"properties": {
"channel": {
"type": "string",
"enum": [
"mail"
]
}
},
"required": [
"channel"
],
"additionalProperties": false
},
"declined": {
"type": "object",
"propertyNames": {
"type": "string",
"enum": [
"rates",
"costs",
"plan",
"days",
"translations",
"accent",
"cover",
"figures",
"tagline",
"intro",
"listed"
]
},
"additionalProperties": {
"type": "string",
"minLength": 10
}
},
"test": {
"type": "boolean"
}
},
"additionalProperties": false
}Responses
- 200
the merged, re-validated document
- 400
invalid_request;invalid_translations;invalid_cover- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
no_such_journal;unknown_trip- 409
stale_document- 413
body_too_large— the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status- 422
incomplete
deleteAsk to delete a trip — mails the owner a single-use confirmation link. Deletes nothing itself.
Responses
- 202
a mail is on its way; nothing has been deleted
- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
no_such_journal- 410
gone- 429
too_many_requests— too many deletion mails to this address recently
putCreate a trip at a client-chosen id, or replace one (with a matching If-Match).
Request body
application/json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"id": {
"type": "string",
"pattern": "^[a-z0-9][a-z0-9-]*$"
},
"title": {
"type": "string",
"minLength": 1,
"maxLength": 200
},
"dates": {
"type": "object",
"properties": {
"from": {
"type": "string",
"pattern": "^\\d{4}-\\d{2}-\\d{2}$"
},
"to": {
"type": "string",
"pattern": "^\\d{4}-\\d{2}-\\d{2}$"
}
},
"required": [
"from",
"to"
],
"additionalProperties": false
},
"visibility": {
"type": "string",
"enum": [
"private",
"public",
"guest"
]
},
"people": {
"minItems": 1,
"maxItems": 10,
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1
},
"nickname": {
"type": "string",
"minLength": 1
},
"email": {
"type": "string",
"format": "email",
"pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
}
},
"required": [
"name",
"email"
],
"additionalProperties": false
}
},
"teaser": {
"type": "boolean"
},
"rates": {
"type": "object",
"properties": {
"currencies": {
"minItems": 1,
"type": "array",
"items": {
"type": "string",
"minLength": 3,
"maxLength": 3
}
},
"manual": {
"type": "object",
"propertyNames": {
"type": "string",
"minLength": 3,
"maxLength": 3
},
"additionalProperties": {
"type": "number",
"exclusiveMinimum": 0
}
}
},
"required": [
"currencies"
],
"additionalProperties": false
},
"costs": {
"type": "object",
"properties": {
"budget": {
"type": "object",
"properties": {
"total": {
"type": "number",
"exclusiveMinimum": 0
},
"days": {
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991
},
"currency": {
"type": "string",
"minLength": 3,
"maxLength": 3
}
},
"required": [
"total"
],
"additionalProperties": false
},
"items": {
"maxItems": 200,
"type": "array",
"items": {
"type": "object",
"properties": {
"label": {
"type": "string",
"minLength": 1,
"maxLength": 200
},
"amount": {
"type": "number",
"exclusiveMinimum": 0
},
"category": {
"type": "string",
"enum": [
"preparation",
"flights",
"accommodation",
"food",
"transport",
"activities",
"other"
]
},
"currency": {
"type": "string",
"minLength": 3,
"maxLength": 3
},
"stop": {
"type": "string"
}
},
"required": [
"label",
"amount"
],
"additionalProperties": false
}
},
"note": {
"type": "string"
},
"visibility": {
"type": "string",
"enum": [
"public",
"guests"
]
}
},
"additionalProperties": false
},
"plan": {
"type": "object",
"properties": {
"route": {
"minItems": 1,
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"pattern": "^[a-z0-9][a-z0-9-]*$"
},
"location": {
"type": "string",
"minLength": 1
},
"lat": {
"type": "number",
"minimum": -90,
"maximum": 90
},
"lng": {
"type": "number",
"minimum": -180,
"maximum": 180
},
"country": {
"type": "string"
},
"countryCode": {
"type": "string",
"minLength": 2,
"maxLength": 2
},
"note": {
"type": "string"
},
"nights": {
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991
},
"arrive": {
"type": "string",
"pattern": "^\\d{4}-\\d{2}-\\d{2}$"
},
"leave": {
"type": "string",
"pattern": "^\\d{4}-\\d{2}-\\d{2}$"
},
"see": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1
},
"lat": {
"type": "number",
"minimum": -90,
"maximum": 90
},
"lng": {
"type": "number",
"minimum": -180,
"maximum": 180
},
"source": {
"type": "string",
"enum": [
"typed",
"maps-link",
"pin",
"coordinates"
]
}
},
"required": [
"name",
"lat",
"lng"
],
"additionalProperties": false
}
},
"source": {
"type": "string",
"enum": [
"typed",
"maps-link",
"pin",
"coordinates"
]
}
},
"required": [
"location",
"lat",
"lng"
],
"additionalProperties": false
}
},
"body": {
"type": "string"
},
"mode": {
"type": "string",
"enum": [
"nights",
"dates"
]
},
"readers": {
"type": "string",
"enum": [
"map",
"details"
]
},
"private": {
"type": "object",
"properties": {
"links": {
"type": "array",
"items": {
"type": "object",
"properties": {
"label": {
"type": "string",
"minLength": 1
},
"url": {
"type": "string",
"format": "uri"
}
},
"required": [
"label",
"url"
],
"additionalProperties": false
}
},
"stops": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "object",
"properties": {
"stay": {
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1
},
"lat": {
"type": "number",
"minimum": -90,
"maximum": 90
},
"lng": {
"type": "number",
"minimum": -180,
"maximum": 180
}
},
"required": [
"name",
"lat",
"lng"
],
"additionalProperties": false
},
"links": {
"type": "array",
"items": {
"type": "object",
"properties": {
"label": {
"type": "string",
"minLength": 1
},
"url": {
"type": "string",
"format": "uri"
}
},
"required": [
"label",
"url"
],
"additionalProperties": false
}
}
},
"additionalProperties": false
}
}
},
"additionalProperties": false
}
},
"required": [
"route"
],
"additionalProperties": false
},
"days": {
"type": "array",
"items": {
"type": "object",
"properties": {
"slug": {
"type": "string",
"pattern": "^\\d{4}-\\d{2}-\\d{2}-[a-z0-9]+(-[a-z0-9]+)*$"
},
"title": {
"type": "string",
"minLength": 1,
"maxLength": 200
},
"date": {
"type": "string",
"pattern": "^\\d{4}-\\d{2}-\\d{2}$"
},
"content": {
"type": "string",
"maxLength": 100000
},
"media": {
"type": "array",
"items": {
"type": "object",
"properties": {
"src": {
"type": "string"
},
"caption": {
"type": "string"
},
"visibility": {
"type": "string",
"enum": [
"guest",
"private"
]
}
},
"required": [
"src"
],
"additionalProperties": false
}
},
"costs": {
"maxItems": 200,
"type": "array",
"items": {
"type": "object",
"properties": {
"label": {
"type": "string",
"minLength": 1,
"maxLength": 200
},
"amount": {
"type": "number",
"exclusiveMinimum": 0
},
"category": {
"type": "string",
"enum": [
"preparation",
"flights",
"accommodation",
"food",
"transport",
"activities",
"other"
]
},
"currency": {
"type": "string",
"minLength": 3,
"maxLength": 3
},
"stop": {
"type": "string"
}
},
"required": [
"label",
"amount"
],
"additionalProperties": false
}
},
"coordinates": {
"type": "object",
"properties": {
"lat": {
"type": "number",
"minimum": -90,
"maximum": 90
},
"lng": {
"type": "number",
"minimum": -180,
"maximum": 180
}
},
"required": [
"lat",
"lng"
],
"additionalProperties": false
},
"weather": {
"anyOf": [
{
"type": "boolean",
"const": true
},
{
"type": "object",
"properties": {
"tempMin": {
"type": "number",
"minimum": -90,
"maximum": 60
},
"tempMax": {
"type": "number",
"minimum": -90,
"maximum": 60
},
"code": {
"type": "integer",
"minimum": 0,
"maximum": 99
},
"precipitation": {
"type": "number",
"minimum": 0,
"maximum": 2000
},
"windMax": {
"type": "number",
"minimum": 0,
"maximum": 500
},
"recordedAt": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
},
"source": {
"type": "string",
"minLength": 1
}
},
"required": [
"recordedAt",
"source"
],
"additionalProperties": false
}
]
},
"time": {
"type": "string",
"pattern": "^([01]\\d|2[0-3]):[0-5]\\d$"
},
"timezone": {
"type": "string"
},
"location": {
"type": "string"
},
"country": {
"type": "string"
},
"countryCode": {
"type": "string",
"minLength": 2,
"maxLength": 2
},
"transportMode": {
"type": "string",
"enum": [
"flight",
"train",
"bus",
"motorbike",
"bicycle",
"boat",
"car",
"taxi",
"walk",
"metro",
"tram",
"ferry"
]
},
"tags": {
"maxItems": 10,
"type": "array",
"items": {
"type": "string",
"maxLength": 30,
"pattern": "^[a-z0-9]+(-[a-z0-9]+)*$"
}
},
"translations": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"content": {
"type": "string"
}
},
"required": [
"title",
"content"
],
"additionalProperties": false
}
},
"visibility": {
"type": "string",
"enum": [
"guest",
"private"
]
},
"status": {
"type": "string",
"const": "draft"
},
"declined": {
"type": "object",
"propertyNames": {
"type": "string",
"enum": [
"media",
"costs",
"coordinates",
"weather",
"time",
"timezone",
"location",
"country",
"countryCode",
"transportMode",
"tags",
"translations",
"visibility",
"status"
]
},
"additionalProperties": {
"type": "string",
"minLength": 10
}
},
"transportFrom": {
"type": "string"
},
"transportTo": {
"type": "string"
},
"travelScene": {
"type": "string",
"enum": [
"default",
"quick",
"skip"
]
},
"test": {
"type": "boolean"
}
},
"required": [
"slug",
"title",
"date",
"content"
],
"additionalProperties": false
}
},
"translations": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"tagline": {
"type": "string"
},
"intro": {
"type": "string"
}
},
"additionalProperties": false
}
},
"accent": {
"type": "string",
"enum": [
"sky",
"yellow",
"green",
"coral",
"navy"
]
},
"cover": {
"type": "string"
},
"figures": {
"anyOf": [
{
"type": "object",
"properties": {
"mode": {
"type": "string",
"const": "off"
}
},
"required": [
"mode"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"mode": {
"type": "string",
"const": "journal"
}
},
"required": [
"mode"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"mode": {
"type": "string",
"const": "custom"
},
"figures": {
"minItems": 1,
"maxItems": 10,
"type": "array",
"items": {
"type": "string",
"minLength": 1
}
}
},
"required": [
"mode",
"figures"
],
"additionalProperties": false
}
]
},
"tagline": {
"type": "string"
},
"intro": {
"type": "string"
},
"listed": {
"type": "boolean"
},
"reminder": {
"type": "object",
"properties": {
"channel": {
"type": "string",
"enum": [
"mail"
]
}
},
"required": [
"channel"
],
"additionalProperties": false
},
"declined": {
"type": "object",
"propertyNames": {
"type": "string",
"enum": [
"rates",
"costs",
"plan",
"days",
"translations",
"accent",
"cover",
"figures",
"tagline",
"intro",
"listed"
]
},
"additionalProperties": {
"type": "string",
"minLength": 10
}
},
"test": {
"type": "boolean"
}
},
"required": [
"id",
"title",
"dates",
"visibility",
"people"
],
"additionalProperties": false,
"x-required-or-declined": [
{
"field": "rates",
"whyRequired": "every trip states the currencies its figures may use, or declines them",
"toDecline": "declined.rates"
},
{
"field": "costs",
"whyRequired": "every trip carries a budget, or says why costs are not tracked here",
"toDecline": "declined.costs"
},
{
"field": "plan",
"whyRequired": "an upcoming trip carries its intended route, or says why there is none",
"toDecline": "declined.plan"
},
{
"field": "days",
"whyRequired": "a trip carries its days, or says why there are none yet (e.g. it has not started)",
"toDecline": "declined.days"
},
{
"field": "translations",
"whyRequired": "a journal that maintains several languages carries each trip's title and tagline in all of them, or says why not (a single-language journal is exempt — the route skips this check)",
"toDecline": "declined.translations"
},
{
"field": "accent",
"whyRequired": "every trip picks the colour its cards are drawn in, or leaves it to the default with a reason",
"toDecline": "declined.accent"
},
{
"field": "figures",
"whyRequired": "how the party is drawn as walking figures: {mode: \"off\"}, {mode: \"journal\"} (the journal's default set), or {mode: \"custom\", figures: [ids]} — create figures at /figures first",
"toDecline": "declined.figures"
},
{
"field": "tagline",
"whyRequired": "every trip card carries its one-line subtitle, or a reason it has none",
"toDecline": "declined.tagline"
},
{
"field": "intro",
"whyRequired": "a trip page opens with a few lines of prose, or says why there are none",
"toDecline": "declined.intro"
}
],
"allOf": [
{
"anyOf": [
{
"required": [
"rates"
]
},
{
"required": [
"declined"
],
"properties": {
"declined": {
"required": [
"rates"
]
}
}
}
]
},
{
"anyOf": [
{
"required": [
"costs"
]
},
{
"required": [
"declined"
],
"properties": {
"declined": {
"required": [
"costs"
]
}
}
}
]
},
{
"anyOf": [
{
"required": [
"plan"
]
},
{
"required": [
"declined"
],
"properties": {
"declined": {
"required": [
"plan"
]
}
}
}
]
},
{
"anyOf": [
{
"required": [
"days"
]
},
{
"required": [
"declined"
],
"properties": {
"declined": {
"required": [
"days"
]
}
}
}
]
},
{
"anyOf": [
{
"required": [
"translations"
]
},
{
"required": [
"declined"
],
"properties": {
"declined": {
"required": [
"translations"
]
}
}
}
]
},
{
"anyOf": [
{
"required": [
"accent"
]
},
{
"required": [
"declined"
],
"properties": {
"declined": {
"required": [
"accent"
]
}
}
}
]
},
{
"anyOf": [
{
"required": [
"figures"
]
},
{
"required": [
"declined"
],
"properties": {
"declined": {
"required": [
"figures"
]
}
}
}
]
},
{
"anyOf": [
{
"required": [
"tagline"
]
},
{
"required": [
"declined"
],
"properties": {
"declined": {
"required": [
"tagline"
]
}
}
}
]
},
{
"anyOf": [
{
"required": [
"intro"
]
},
{
"required": [
"declined"
],
"properties": {
"declined": {
"required": [
"intro"
]
}
}
}
]
}
]
}Responses
- 200
replaced (If-Match matched the stored ETag)
- 201
created — carries
nextwhen this is the journal's first trip- 400
invalid_trip_id;invalid_request;invalid_trip;invalid_translations;invalid_cover- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
no_such_journal- 409
stale_document— exists already, with no or a stale If-Match — carries the stored document- 413
body_too_large— the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status- 422
incomplete
/api/v2/{user}/trips/{trip}/rename
postRename a trip's id — the folder, trip.json's own id, every database row a trip_id column names, and a redirect record so the old address keeps answering. Never touches gps/.
Request body
application/json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"id": {
"type": "string",
"pattern": "^[a-z0-9][a-z0-9-]*$"
}
},
"required": [
"id"
],
"additionalProperties": false
}Responses
- 200
renamed — GET the trip at its new id from here on
- 400
invalid_trip_id— not a valid slug, or the same id the trip already has;invalid_request— the new id is the trip's current one — nothing to rename- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
no_such_journal;unknown_trip- 409
trip_id_taken- 413
body_too_large— the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status
/api/v2/{user}/trips/{trip}/days
getEvery day in this trip, paged.
Responses
- 200
one page of day documents
- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
unknown_trip
/api/v2/{user}/trips/{trip}/days/{slug}
getOne day.
Responses
- 200
the day, with its ETag
- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
unknown_trip;unknown_day
patchMerge-patch a day. Never moves it between draft and published. A draft may stay incomplete — completeness is asked at publish; a published day must stay complete (422 incomplete). A day that changed while the patch was checked (a publish, another write) answers 409 stale_document.
Request body
application/json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"slug": {
"type": "string",
"pattern": "^\\d{4}-\\d{2}-\\d{2}-[a-z0-9]+(-[a-z0-9]+)*$"
},
"title": {
"type": "string",
"minLength": 1,
"maxLength": 200
},
"date": {
"type": "string",
"pattern": "^\\d{4}-\\d{2}-\\d{2}$"
},
"content": {
"type": "string",
"maxLength": 100000
},
"media": {
"type": "array",
"items": {
"type": "object",
"properties": {
"src": {
"type": "string"
},
"caption": {
"type": "string"
},
"visibility": {
"type": "string",
"enum": [
"guest",
"private"
]
}
},
"required": [
"src"
],
"additionalProperties": false
}
},
"costs": {
"maxItems": 200,
"type": "array",
"items": {
"type": "object",
"properties": {
"label": {
"type": "string",
"minLength": 1,
"maxLength": 200
},
"amount": {
"type": "number",
"exclusiveMinimum": 0
},
"category": {
"type": "string",
"enum": [
"preparation",
"flights",
"accommodation",
"food",
"transport",
"activities",
"other"
]
},
"currency": {
"type": "string",
"minLength": 3,
"maxLength": 3
},
"stop": {
"type": "string"
}
},
"required": [
"label",
"amount"
],
"additionalProperties": false
}
},
"coordinates": {
"type": "object",
"properties": {
"lat": {
"type": "number",
"minimum": -90,
"maximum": 90
},
"lng": {
"type": "number",
"minimum": -180,
"maximum": 180
}
},
"required": [
"lat",
"lng"
],
"additionalProperties": false
},
"weather": {
"anyOf": [
{
"type": "boolean",
"const": true
},
{
"type": "object",
"properties": {
"tempMin": {
"type": "number",
"minimum": -90,
"maximum": 60
},
"tempMax": {
"type": "number",
"minimum": -90,
"maximum": 60
},
"code": {
"type": "integer",
"minimum": 0,
"maximum": 99
},
"precipitation": {
"type": "number",
"minimum": 0,
"maximum": 2000
},
"windMax": {
"type": "number",
"minimum": 0,
"maximum": 500
},
"recordedAt": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
},
"source": {
"type": "string",
"minLength": 1
}
},
"required": [
"recordedAt",
"source"
],
"additionalProperties": false
}
]
},
"time": {
"type": "string",
"pattern": "^([01]\\d|2[0-3]):[0-5]\\d$"
},
"timezone": {
"type": "string"
},
"location": {
"type": "string"
},
"country": {
"type": "string"
},
"countryCode": {
"type": "string",
"minLength": 2,
"maxLength": 2
},
"transportMode": {
"type": "string",
"enum": [
"flight",
"train",
"bus",
"motorbike",
"bicycle",
"boat",
"car",
"taxi",
"walk",
"metro",
"tram",
"ferry"
]
},
"tags": {
"maxItems": 10,
"type": "array",
"items": {
"type": "string",
"maxLength": 30,
"pattern": "^[a-z0-9]+(-[a-z0-9]+)*$"
}
},
"translations": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"content": {
"type": "string"
}
},
"required": [
"title",
"content"
],
"additionalProperties": false
}
},
"visibility": {
"type": "string",
"enum": [
"guest",
"private"
]
},
"status": {
"type": "string",
"const": "draft"
},
"declined": {
"type": "object",
"propertyNames": {
"type": "string",
"enum": [
"media",
"costs",
"coordinates",
"weather",
"time",
"timezone",
"location",
"country",
"countryCode",
"transportMode",
"tags",
"translations",
"visibility",
"status"
]
},
"additionalProperties": {
"type": "string",
"minLength": 10
}
},
"transportFrom": {
"type": "string"
},
"transportTo": {
"type": "string"
},
"travelScene": {
"type": "string",
"enum": [
"default",
"quick",
"skip"
]
},
"test": {
"type": "boolean"
}
},
"additionalProperties": false
}Responses
- 200
the merged, re-validated document
- 400
invalid_request;weather_disabled;invalid_entry;invalid_translations- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
unknown_trip;unknown_day- 409
stale_document- 413
body_too_large— the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status- 422
incomplete
deleteDelete a draft day outright — no confirmation step, since it was never on the site.
Responses
- 200
deleted; its media is kept on disk
- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
unknown_trip;unknown_day- 409
published_day_not_deletable
putCreate a day at a client-chosen slug, or replace a draft (with a matching If-Match).
Request body
application/json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"slug": {
"type": "string",
"pattern": "^\\d{4}-\\d{2}-\\d{2}-[a-z0-9]+(-[a-z0-9]+)*$"
},
"title": {
"type": "string",
"minLength": 1,
"maxLength": 200
},
"date": {
"type": "string",
"pattern": "^\\d{4}-\\d{2}-\\d{2}$"
},
"content": {
"type": "string",
"maxLength": 100000
},
"media": {
"type": "array",
"items": {
"type": "object",
"properties": {
"src": {
"type": "string"
},
"caption": {
"type": "string"
},
"visibility": {
"type": "string",
"enum": [
"guest",
"private"
]
}
},
"required": [
"src"
],
"additionalProperties": false
}
},
"costs": {
"maxItems": 200,
"type": "array",
"items": {
"type": "object",
"properties": {
"label": {
"type": "string",
"minLength": 1,
"maxLength": 200
},
"amount": {
"type": "number",
"exclusiveMinimum": 0
},
"category": {
"type": "string",
"enum": [
"preparation",
"flights",
"accommodation",
"food",
"transport",
"activities",
"other"
]
},
"currency": {
"type": "string",
"minLength": 3,
"maxLength": 3
},
"stop": {
"type": "string"
}
},
"required": [
"label",
"amount"
],
"additionalProperties": false
}
},
"coordinates": {
"type": "object",
"properties": {
"lat": {
"type": "number",
"minimum": -90,
"maximum": 90
},
"lng": {
"type": "number",
"minimum": -180,
"maximum": 180
}
},
"required": [
"lat",
"lng"
],
"additionalProperties": false
},
"weather": {
"anyOf": [
{
"type": "boolean",
"const": true
},
{
"type": "object",
"properties": {
"tempMin": {
"type": "number",
"minimum": -90,
"maximum": 60
},
"tempMax": {
"type": "number",
"minimum": -90,
"maximum": 60
},
"code": {
"type": "integer",
"minimum": 0,
"maximum": 99
},
"precipitation": {
"type": "number",
"minimum": 0,
"maximum": 2000
},
"windMax": {
"type": "number",
"minimum": 0,
"maximum": 500
},
"recordedAt": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
},
"source": {
"type": "string",
"minLength": 1
}
},
"required": [
"recordedAt",
"source"
],
"additionalProperties": false
}
]
},
"time": {
"type": "string",
"pattern": "^([01]\\d|2[0-3]):[0-5]\\d$"
},
"timezone": {
"type": "string"
},
"location": {
"type": "string"
},
"country": {
"type": "string"
},
"countryCode": {
"type": "string",
"minLength": 2,
"maxLength": 2
},
"transportMode": {
"type": "string",
"enum": [
"flight",
"train",
"bus",
"motorbike",
"bicycle",
"boat",
"car",
"taxi",
"walk",
"metro",
"tram",
"ferry"
]
},
"tags": {
"maxItems": 10,
"type": "array",
"items": {
"type": "string",
"maxLength": 30,
"pattern": "^[a-z0-9]+(-[a-z0-9]+)*$"
}
},
"translations": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"content": {
"type": "string"
}
},
"required": [
"title",
"content"
],
"additionalProperties": false
}
},
"visibility": {
"type": "string",
"enum": [
"guest",
"private"
]
},
"status": {
"type": "string",
"const": "draft"
},
"declined": {
"type": "object",
"propertyNames": {
"type": "string",
"enum": [
"media",
"costs",
"coordinates",
"weather",
"time",
"timezone",
"location",
"country",
"countryCode",
"transportMode",
"tags",
"translations",
"visibility",
"status"
]
},
"additionalProperties": {
"type": "string",
"minLength": 10
}
},
"transportFrom": {
"type": "string"
},
"transportTo": {
"type": "string"
},
"travelScene": {
"type": "string",
"enum": [
"default",
"quick",
"skip"
]
},
"test": {
"type": "boolean"
}
},
"required": [
"slug",
"title",
"date",
"content"
],
"additionalProperties": false,
"x-required-or-declined": [
{
"field": "media",
"whyRequired": "a day names the photographs on it (by the src the media door answered with), or says why there are none",
"toDecline": "declined.media"
},
{
"field": "costs",
"whyRequired": "a day carries what was spent on it, or says why not (nothing spent / figures lost / owner tracks costs elsewhere)",
"toDecline": "declined.costs"
},
{
"field": "coordinates",
"whyRequired": "a day carries where it happened (lat/lng), or says why there is no position",
"toDecline": "declined.coordinates"
},
{
"field": "weather",
"whyRequired": "a day asks the server to look its weather up (weather: true), brings a real reading, or declines",
"toDecline": "declined.weather"
},
{
"field": "time",
"whyRequired": "a day says when it happened (HH:MM, local), or declines",
"toDecline": "declined.time"
},
{
"field": "timezone",
"whyRequired": "the IANA zone the time is local to, or a reason it is not known",
"toDecline": "declined.timezone"
},
{
"field": "location",
"whyRequired": "the place this day happened, or why none is named",
"toDecline": "declined.location"
},
{
"field": "country",
"whyRequired": "the country this day happened in, or why none is named",
"toDecline": "declined.country"
},
{
"field": "countryCode",
"whyRequired": "the ISO-2 code behind the flag the day card draws, or a decline",
"toDecline": "declined.countryCode"
},
{
"field": "transportMode",
"whyRequired": "how this day travelled, or a decline for a day with no leg (a rest day)",
"toDecline": "declined.transportMode"
},
{
"field": "tags",
"whyRequired": "the day's tags, or a decline",
"toDecline": "declined.tags"
},
{
"field": "translations",
"whyRequired": "a journal that maintains several languages carries the day in all of them, or declines — a declined translation falls back to the main language (a single-language journal is exempt; the route skips this check)",
"toDecline": "declined.translations"
},
{
"field": "visibility",
"whyRequired": "whether this day is held back (guest or private, narrowing the trip's own gate), or declined — declined means shown to everyone the trip lets in",
"toDecline": "declined.visibility"
},
{
"field": "status",
"whyRequired": "a day states it arrives as a draft — status: \"draft\" is the only writable value; publishing stays its own call",
"toDecline": "declined.status"
}
],
"allOf": [
{
"anyOf": [
{
"required": [
"media"
]
},
{
"required": [
"declined"
],
"properties": {
"declined": {
"required": [
"media"
]
}
}
}
]
},
{
"anyOf": [
{
"required": [
"costs"
]
},
{
"required": [
"declined"
],
"properties": {
"declined": {
"required": [
"costs"
]
}
}
}
]
},
{
"anyOf": [
{
"required": [
"coordinates"
]
},
{
"required": [
"declined"
],
"properties": {
"declined": {
"required": [
"coordinates"
]
}
}
}
]
},
{
"anyOf": [
{
"required": [
"weather"
]
},
{
"required": [
"declined"
],
"properties": {
"declined": {
"required": [
"weather"
]
}
}
}
]
},
{
"anyOf": [
{
"required": [
"time"
]
},
{
"required": [
"declined"
],
"properties": {
"declined": {
"required": [
"time"
]
}
}
}
]
},
{
"anyOf": [
{
"required": [
"timezone"
]
},
{
"required": [
"declined"
],
"properties": {
"declined": {
"required": [
"timezone"
]
}
}
}
]
},
{
"anyOf": [
{
"required": [
"location"
]
},
{
"required": [
"declined"
],
"properties": {
"declined": {
"required": [
"location"
]
}
}
}
]
},
{
"anyOf": [
{
"required": [
"country"
]
},
{
"required": [
"declined"
],
"properties": {
"declined": {
"required": [
"country"
]
}
}
}
]
},
{
"anyOf": [
{
"required": [
"countryCode"
]
},
{
"required": [
"declined"
],
"properties": {
"declined": {
"required": [
"countryCode"
]
}
}
}
]
},
{
"anyOf": [
{
"required": [
"transportMode"
]
},
{
"required": [
"declined"
],
"properties": {
"declined": {
"required": [
"transportMode"
]
}
}
}
]
},
{
"anyOf": [
{
"required": [
"tags"
]
},
{
"required": [
"declined"
],
"properties": {
"declined": {
"required": [
"tags"
]
}
}
}
]
},
{
"anyOf": [
{
"required": [
"translations"
]
},
{
"required": [
"declined"
],
"properties": {
"declined": {
"required": [
"translations"
]
}
}
}
]
},
{
"anyOf": [
{
"required": [
"visibility"
]
},
{
"required": [
"declined"
],
"properties": {
"declined": {
"required": [
"visibility"
]
}
}
}
]
},
{
"anyOf": [
{
"required": [
"status"
]
},
{
"required": [
"declined"
],
"properties": {
"declined": {
"required": [
"status"
]
}
}
}
]
}
]
}Responses
- 200
replaced
- 201
created — carries
nextwhen this is the trip's first day- 400
invalid_request;weather_disabled;invalid_entry;invalid_translations- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
unknown_trip- 409
stale_document;already_published— PUT replaces a draft only; PATCH a published day- 413
body_too_large— the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status- 422
incomplete
/api/v2/{user}/trips/{trip}/days/{slug}/media
postAttach already-stored photographs to this day's gallery — never uploads bytes itself (POST /api/v2/{user}/media does that first). Retracts a stale declined.media.
Request body
application/json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"items": {
"minItems": 1,
"type": "array",
"items": {
"type": "object",
"properties": {
"src": {
"type": "string",
"minLength": 1
},
"caption": {
"type": "string"
},
"visibility": {
"type": "string",
"enum": [
"guest",
"private"
]
}
},
"required": [
"src"
],
"additionalProperties": false
}
}
},
"required": [
"items"
],
"additionalProperties": false
}Responses
- 200
the day, with the photographs attached
- 400
invalid_request;not_this_trip- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
unknown_trip;unknown_day- 409
stale_document- 413
body_too_large— the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status
deleteTake photographs off this day's gallery by src — the reversible half; the bytes stay on disk (DELETE /api/v2/{user}/media removes those, and detaches from every day too).
Request body
application/json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"srcs": {
"minItems": 1,
"type": "array",
"items": {
"type": "string",
"minLength": 1
}
}
},
"required": [
"srcs"
],
"additionalProperties": false
}Responses
- 200
the day, with the photographs detached
- 400
invalid_request- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
unknown_trip;unknown_day;unknown_media- 409
stale_document- 413
body_too_large— the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status
/api/v2/{user}/trips/{trip}/days/{slug}/publish
postOwner only: put a draft day on the site. declineTracked may name only fields the day has neither filled in nor already answered; any other name refuses the whole call (400, details.refused).
Request body
application/json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"declineTracked": {
"type": "array",
"items": {
"type": "string",
"enum": [
"media",
"costs",
"coordinates",
"weather",
"time",
"timezone",
"location",
"country",
"countryCode",
"transportMode",
"tags",
"translations",
"visibility",
"status"
]
}
},
"sendMail": {
"type": "boolean"
},
"sendWhatsapp": {
"type": "boolean"
}
},
"additionalProperties": false
}Responses
- 200
dryRun — nothing written
- 400
invalid_request— includes declineTracked naming a field that is not blank (details.refused)- 401
missing_token;invalid_token- 402
no_credits- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it;out_of_scope— a trip-scoped token cannot publish- 404
unknown_trip;unknown_day- 409
already_published;stale_document— the day changed while the publish was checked; nothing written- 413
body_too_large— the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status- 422
incomplete_day
/api/v2/{user}/trips/{trip}/days/{slug}/unpublish
postOwner only: take a published day off the site. Reversible by publishing again.
Responses
- 200
off the site
- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it;out_of_scope— a trip-scoped token cannot unpublish- 404
unknown_trip;unknown_day- 409
already_draft
/api/v2/{user}/trips/{trip}/days/{slug}/send
postOwner only: send (or resend) a published day by mail and/or WhatsApp. The one send door.
Request body
application/json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"channels": {
"minItems": 1,
"type": "array",
"items": {
"type": "string",
"enum": [
"mail",
"whatsapp"
]
}
}
},
"required": [
"channels"
],
"additionalProperties": false
}Responses
- 200
per-channel send summaries
- 400
invalid_request- 401
missing_token;invalid_token- 402
no_credits- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it;out_of_scope— a trip-scoped token cannot send- 404
unknown_trip;unknown_day- 409
not_published;test_content- 413
body_too_large— the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status
/api/v2/{user}/trips/{trip}/days/{slug}/move
postMove a day to a different date and/or a different trip, keeping its media the same print masters.
Request body
application/json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"toTripId": {
"type": "string",
"pattern": "^[a-z0-9][a-z0-9-]*$"
},
"date": {
"type": "string",
"pattern": "^\\d{4}-\\d{2}-\\d{2}$"
}
},
"required": [
"toTripId",
"date"
],
"additionalProperties": false
}Responses
- 200
moved — GET the returned tripId/slug to see it
- 400
invalid_date;invalid_request- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
no_such_journal;unknown_trip;unknown_day- 409
already_exists- 413
body_too_large— the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status
/api/v2/{user}/trips/{trip}/days/{slug}/split
postSplit one day's update into two, on the same date. The prose is never cut automatically.
Request body
application/json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"photoCutIndex": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991
},
"firstContent": {
"type": "string"
},
"secondTitle": {
"type": "string"
},
"secondContent": {
"type": "string"
},
"secondTime": {
"type": "string",
"pattern": "^([01]\\d|2[0-3]):[0-5]\\d$"
}
},
"required": [
"photoCutIndex",
"firstContent",
"secondTitle",
"secondContent"
],
"additionalProperties": false
}Responses
- 200
split — GET secondSlug to see the new half, always a draft
- 400
invalid_request- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
no_such_journal;unknown_trip;unknown_day- 409
title_required;slug_taken- 413
body_too_large— the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status
/api/v2/{user}/trips/{trip}/days/{slug}/merge
postJoin two updates on the same trip into one. Refused across trips.
Request body
application/json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"withTripId": {
"type": "string",
"pattern": "^[a-z0-9][a-z0-9-]*$"
},
"withSlug": {
"type": "string",
"pattern": "^\\d{4}-\\d{2}-\\d{2}-[a-z0-9]+(-[a-z0-9]+)*$"
}
},
"required": [
"withSlug"
],
"additionalProperties": false
}Responses
- 200
merged — GET the returned slug to see it
- 400
invalid_request- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
no_such_journal;unknown_trip;unknown_day- 409
cross_trip- 413
body_too_large— the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status
/api/v2/{user}/trips/{trip}/costs/apply
postWrite agreed bank-statement rows onto the days they belong to — a row whose date has no day yet is filed to the trip's own costs.items instead, never dropped.
Request body
application/json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"rows": {
"minItems": 1,
"type": "array",
"items": {
"type": "object",
"properties": {
"date": {
"type": "string",
"pattern": "^\\d{4}-\\d{2}-\\d{2}$"
},
"label": {
"type": "string",
"minLength": 1
},
"amount": {
"type": "number",
"exclusiveMinimum": 0
},
"currency": {
"type": "string",
"minLength": 3,
"maxLength": 3
},
"category": {
"type": "string",
"enum": [
"preparation",
"flights",
"accommodation",
"food",
"transport",
"activities",
"other"
]
}
},
"required": [
"date",
"label",
"amount",
"currency",
"category"
],
"additionalProperties": false
}
},
"statement": {
"type": "string"
}
},
"required": [
"rows"
],
"additionalProperties": false
}Responses
- 200
how many rows landed on a day, and which dates were filed to the trip instead
- 400
invalid_costs- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
unknown_trip- 413
body_too_large— the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status
/api/v2/{user}/media
getOne trip's stored media, paged — send ?trip=.
Responses
- 200
one page of media items
- 400
invalid_request- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
unknown_trip
postUpload bytes: JSON `{intent, url|inbox}` fetching/resolving the bytes, or `multipart/form-data` with `file` and `intent`. `intent.kind` is one of photo, bank_export, gps_history, document.
Request body
application/json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"kind": {
"type": "string",
"enum": [
"photo",
"bank_export",
"gps_history",
"document"
]
},
"trip": {
"type": "string",
"minLength": 1
},
"day": {
"type": "string",
"minLength": 1
},
"caption": {
"type": "string",
"minLength": 1
},
"format": {
"type": "string",
"minLength": 1
},
"declined": {
"type": "object",
"propertyNames": {
"type": "string",
"enum": [
"trip",
"day",
"caption",
"format"
]
},
"additionalProperties": {
"type": "string",
"minLength": 10
}
}
},
"required": [
"kind"
],
"additionalProperties": false
}Responses
- 201
stored
- 400
invalid_request;expected_urls;unknown_inbox_file;could_not_fetch;expected_multipart;storage_full;invalid_media- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
unknown_trip;unknown_day- 413
body_too_large
deleteRemove a stored media item by its src.
Responses
- 200
removed
- 400
invalid_request- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
unknown_media
/api/v2/{user}/trips/{trip}/media/duplicates
getThe same photograph, twice — reports only, never deletes.
Responses
- 200
groups of look-alike photographs, largest first
- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
unknown_trip
/api/v2/{user}/gps/zones
getThe journal's private zones — a home, a place clipped out of every future track.
Responses
- 200
the zones, the decline flag, the limits, and an ETag — send it back as If-Match on PUT
- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
no_such_journal- 500
unreadable_zones
putReplace the zone list. Requires If-Match with the ETag GET last answered — refused without one, or with a stale one, as stale_document (409). Label a zone exactly "home" to arm the recorder's own gate (B2196/B2198).
Request body
application/json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"zones": {
"maxItems": 20,
"type": "array",
"items": {
"type": "object",
"properties": {
"label": {
"type": "string",
"minLength": 1,
"maxLength": 80
},
"lat": {
"type": "number",
"minimum": -90,
"maximum": 90
},
"lon": {
"type": "number",
"minimum": -180,
"maximum": 180
},
"radiusM": {
"type": "number",
"minimum": 50,
"maximum": 5000
}
},
"required": [
"label",
"lat",
"lon",
"radiusM"
],
"additionalProperties": false
}
},
"homeDeclined": {
"type": "boolean"
}
},
"required": [
"zones"
],
"additionalProperties": false
}Responses
- 200
the zones, the decline flag, and the limits, as stored, with the new ETag
- 400
invalid_request- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
no_such_journal- 409
stale_document— no If-Match, or one that does not cover the current ETag — carries the stored document- 413
body_too_large— the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status- 500
unreadable_zones
/api/v2/{user}/inbox
getFiles staged with no trip/day yet, plus every photograph filed onto a trip with a declined day.
Responses
- 200
counts and items with no trip yet, by shelf; tripWaiting for the trip-scoped, still day-less kind
- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
no_such_journal
/api/v2/{user}/inbox/{id}
deleteDiscard one staged file. No confirmation — nothing here has ever been on the site.
Responses
- 200
removed
- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
no_such_journal;not_found
/api/v2/{user}/figures
getEvery figure in the journal's library, paged.
Responses
- 200
one page of figures
- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
no_such_journal
/api/v2/{user}/figures/{id}
getOne figure.
Responses
- 200
the figure
- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
no_such_journal;not_found
deleteDelete a figure — refused while any trip or the journal's own default set still names it.
Responses
- 200
deleted
- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
no_such_journal;not_found- 409
figure_referenced
putCreate a figure at a client-chosen id, or replace one (with a matching If-Match).
Request body
application/json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"id": {
"type": "string",
"pattern": "^[a-z0-9][a-z0-9-]*$"
},
"name": {
"type": "string",
"minLength": 1
},
"person": {
"type": "string",
"format": "email",
"pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
},
"hairStyle": {
"type": "string",
"enum": [
"buzz",
"short",
"tousled",
"long",
"curly",
"coils",
"braids",
"bun",
"ponytail",
"bald",
"headscarf"
]
},
"outfit": {
"type": "string",
"enum": [
"trousers",
"shorts",
"skirt",
"dress",
"robe"
]
},
"build": {
"type": "string",
"enum": [
"slight",
"average",
"broad"
]
},
"age": {
"type": "string",
"enum": [
"child",
"teen",
"adult",
"elder"
]
},
"skin": {
"type": "string",
"minLength": 1
},
"hair": {
"type": "string",
"minLength": 1
},
"eyes": {
"type": "string",
"minLength": 1
},
"shirt": {
"type": "string",
"minLength": 1
},
"pants": {
"type": "string",
"minLength": 1
},
"pack": {
"type": "string",
"minLength": 1
},
"headscarf": {
"type": "string",
"minLength": 1
},
"accessories": {
"type": "array",
"items": {
"type": "string",
"enum": [
"glasses",
"sunglasses",
"hat",
"cap",
"beanie",
"scarf",
"camera",
"stick"
]
}
}
},
"required": [
"id"
],
"additionalProperties": false
}Responses
- 200
replaced
- 201
created
- 400
invalid_request- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
no_such_journal- 409
stale_document- 413
body_too_large— the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status
/api/v2/{user}/figures/presets
getThe whole vocabulary a figure may be described in, and starting points.
Responses
- 200
the vocabulary
- 404
no_such_journal
/api/v2/{user}/figures/preview
getDraw one figure (?figure=) or a party (?party=) as an SVG, so a person can confirm it before it is written.
Responses
- 200
an SVG drawing
- 400
nothing_to_draw;invalid_json- 404
no_such_journal
/api/v2/{user}/trips/{trip}/travellers/from-photo
postRead a party off a group photograph — proposed, never written. Multipart `photo`, or JSON `{inbox}`/`{gallery}`.
Responses
- 200
a proposed party, ordered left to right in the photograph
- 400
expected_photo;invalid_media;invalid_json;unknown_inbox_file;not_this_trip- 401
missing_token;invalid_token- 402
no_credits- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it;consent_required- 404
unknown_trip;helper_unavailable- 429
too_many_requests- 502
model_failed
/api/v2/{user}/purchases
getThis journal's purchase history, paged.
Responses
- 200
one page of purchases
- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
no_such_journal;credits_disabled
/api/v2/{user}/purchases/{id}
getOne purchase.
Responses
- 200
the purchase
- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
no_such_journal;credits_disabled;unknown_payment
putPropose buying credits at a client-chosen id. Files a pending transaction and mails the owner — grants nothing itself.
Request body
application/json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"credits": {
"type": "integer",
"minimum": 10,
"maximum": 500
}
},
"required": [
"credits"
],
"additionalProperties": false
}Responses
- 200
the same id was already this exact amount — a no-op re-read
- 201
created; mail sent
- 400
invalid_amount- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
no_such_journal;credits_disabled- 409
no_owner_address;conflict— the id exists with a different amount- 413
body_too_large— the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status- 429
too_many_requests- 503
no_database
/api/v2/{user}/credits/ledger
getThis journal's credit ledger, paged.
Responses
- 200
one page of ledger rows
- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
no_such_journal;credits_disabled
/api/v2/{user}/contacts/import
postFile many pending contacts at once, from rows a person already agreed.
Request body
application/json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"rows": {
"minItems": 1,
"type": "array",
"items": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
}
}
},
"required": [
"rows"
],
"additionalProperties": false
}Responses
- 200
per-row outcomes
- 400
invalid_request- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
no_such_journal- 409
contacts_disabled- 413
body_too_large— the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status
/api/v2/{user}/contacts/self
postAdd the owner as their own contact, from this journal's own config.json.
Responses
- 200
added, or already present
- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
no_such_journal- 409
contacts_disabled;invalid_request
/api/v2/{user}/statements/{src}
getRead a staged bank export as a report — writes nothing.
Responses
- 200
merchants, payments and rates
- 400
unreadable_statement- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
no_such_journal;unknown_statement
/api/v2/{user}/postcards/recipients
getWho a postcard may be addressed to — a name, town and country, never a street.
Responses
- 200
eligible recipients
- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
postcards_disabled;no_such_journal
/api/v2/{user}/postcards/texts
getPrefill material for a card's message — every day's opening line, per locale. Send ?trip=.
Responses
- 200
opening lines by day and locale
- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
postcards_disabled;no_such_journal;unknown_trip
/api/v2/{user}/postcards/orders/{id}
getOne postcard order.
Responses
- 200
the order
- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
postcards_disabled;contacts_disabled;no_such_journal;unknown_order
putPropose a postcard order at a client-chosen id. Charges nothing and prints nothing — the owner presses Send.
Request body
application/json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"source": {
"anyOf": [
{
"type": "object",
"properties": {
"trip": {
"type": "string",
"minLength": 1
},
"day": {
"type": "string",
"minLength": 1
},
"photo": {
"type": "string",
"minLength": 1
}
},
"required": [
"trip",
"day",
"photo"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"inbox": {
"type": "string",
"minLength": 1
}
},
"required": [
"inbox"
],
"additionalProperties": false
}
]
},
"message": {
"type": "string",
"minLength": 1,
"maxLength": 600
},
"from": {
"type": "string",
"minLength": 1,
"maxLength": 120
},
"recipients": {
"minItems": 1,
"maxItems": 25,
"type": "array",
"items": {
"type": "string",
"minLength": 1
}
},
"locale": {
"type": "string",
"minLength": 1
},
"crop": {
"type": "object",
"properties": {
"x": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"y": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"zoom": {
"type": "number",
"minimum": 1
}
},
"required": [
"x",
"y"
],
"additionalProperties": false
},
"figures": {
"type": "boolean"
}
},
"required": [
"source",
"message",
"from",
"recipients"
],
"additionalProperties": false
}Responses
- 200
the id already exists — echoed back, nothing rewritable through this door
- 201
proposed
- 400
invalid_request;test_content- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
postcards_disabled;contacts_disabled;no_such_journal;unknown_photo;unknown_trip;unknown_day;unknown_recipient- 409
stale_document- 413
body_too_large— the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status- 503
no_database
/api/v2/{user}/photobooks/drafts/{trip}
getOne trip's photobook arrangement, whole — every option, and the limits each is checked against.
Responses
- 200
the arrangement as stored
- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
photobook_disabled;no_such_journal;unknown_trip;not_found
putArrange a book. Fields sent land on top of what is stored, or on the defaults; the whole arrangement comes back. Charges nothing, builds nothing, prints nothing — the owner opens `url` to price and buy it.
Request body
application/json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"options": {
"type": "object",
"properties": {
"size": {
"type": "string",
"enum": [
"pocket",
"square",
"portrait",
"large-square"
]
},
"coverType": {
"type": "string",
"enum": [
"soft",
"hard"
]
},
"locale": {
"type": "string"
},
"excludePhotos": {
"maxItems": 20000,
"type": "array",
"items": {
"type": "string",
"maxLength": 300
}
},
"days": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "object",
"properties": {
"photos": {
"maxItems": 500,
"type": "array",
"items": {
"type": "string",
"maxLength": 300
}
},
"keep": {
"maxItems": 500,
"type": "array",
"items": {
"type": "string",
"maxLength": 300
}
},
"layout": {
"type": "string",
"enum": [
"auto",
"hero",
"single",
"pair",
"grid",
"text"
]
},
"hero": {
"type": "string",
"maxLength": 300
},
"runOn": {
"type": "boolean"
},
"excluded": {
"type": "boolean"
},
"text": {
"type": "boolean"
},
"textForm": {
"type": "string",
"enum": [
"runon-auto",
"textpage"
]
}
},
"additionalProperties": false
}
},
"includeText": {
"type": "boolean"
},
"includeMap": {
"type": "boolean"
},
"mapStyle": {
"type": "string",
"enum": [
"auto",
"closeup",
"numbered",
"chapters",
"countries",
"strip"
]
},
"mapPlaces": {
"type": "boolean"
},
"mapTerrain": {
"type": "boolean"
},
"includeChapters": {
"type": "boolean"
},
"includeNames": {
"type": "boolean"
},
"includeFollowers": {
"type": "boolean"
},
"includeTransport": {
"type": "boolean"
},
"includeCosts": {
"type": "boolean"
},
"includeCharts": {
"type": "boolean"
},
"photoBudget": {
"type": "string",
"enum": [
"highlights",
"balanced",
"comprehensive"
]
},
"includeFigureMarks": {
"type": "boolean"
},
"includeVehicles": {
"type": "boolean"
},
"cover": {
"type": "string",
"maxLength": 300
},
"spineText": {
"type": "string",
"maxLength": 60
},
"focalPoints": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "object",
"properties": {
"x": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"y": {
"type": "number",
"minimum": 0,
"maximum": 1
}
},
"required": [
"x",
"y"
],
"additionalProperties": false
}
},
"titleStyle": {
"type": "string",
"enum": [
"journal",
"magazine",
"window",
"tone",
"places",
"fullbleed",
"map",
"grid"
]
},
"introOnTitle": {
"type": "boolean"
},
"titlePhoto": {
"type": "string",
"maxLength": 300
},
"titlePhotos": {
"maxItems": 4,
"type": "array",
"items": {
"type": "string",
"maxLength": 300
}
},
"chapterStyle": {
"type": "string",
"enum": [
"stats",
"typeonly",
"fullbleed",
"map"
]
},
"backStyle": {
"type": "string",
"enum": [
"route",
"photo",
"mosaic",
"quiet"
]
},
"backPhoto": {
"type": "string",
"maxLength": 300
}
},
"additionalProperties": false
}
},
"required": [
"options"
],
"additionalProperties": false
}Responses
- 200
the arrangement, merged and stored
- 201
the first arrangement for this trip
- 400
invalid_request- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
photobook_disabled;no_such_journal;unknown_trip- 409
stale_document- 413
body_too_large— the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status- 503
no_database
/api/v2/{user}/photobooks/orders/{id}
getOne photobook order. Read-only — building and paying for a book is browser-only.
Responses
- 200
the order
- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
photobook_disabled;no_such_journal;unknown_order
/api/v2/{user}/trips/{trip}/track
postDraw this trip's line from the imported location history — owner only.
Responses
- 200
segments, points and how many private zones were cut out.
written: falsemeans nothing was stored for these dates, and any existing line was left alone- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip;forbidden— this call is the journal owner's; a trip-scoped token cannot do it- 404
unknown_trip
/api/v2/{user}/deletions/{token}
postConfirm a deletion — the button on the mailed page, never something an agent calls.
Responses
- 200
deleted
- 404
not_found— no such token for this journal- 409
deletion_link_used;deletion_link_expired- 410
gone— what it pointed at has already gone- 429
too_many_requests
/api/auth/codes
postAsk for a one-time code — one door for four credentials.
Request body
application/json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"email": {
"type": "string"
},
"phone": {
"type": "string"
},
"for": {
"type": "string",
"enum": [
"read",
"write",
"identity",
"signup"
]
},
"user": {
"type": "string"
},
"scope": {
"type": "object",
"properties": {
"trip": {
"type": "string"
}
},
"required": [
"trip"
],
"additionalProperties": false
},
"channel": {
"type": "string",
"enum": [
"mail"
]
},
"destination": {
"type": "string"
},
"locale": {
"type": "string"
}
},
"required": [
"for"
],
"additionalProperties": false
}Responses
- 202
accepted — always, whatever the address
- 400
invalid_request— a shape rule, never address-dependent;invalid_email;sms_unreachable—phonein a country this server's SMS number cannot reach- 403
not_authorised— for: "write" to an address that owns nothing and is on no named trip;signup_not_invited- 404
signup_disabled;auth_disabled- 429
too_many_requests— narrower for write than for read- 503
mail_disabled— nothing issued; any code already held is still live;sms_disabled—phoneasked for, and this server sends no SMS;mail_failed
/api/auth/codes/redeem
postSpend a code — one door for four credentials.
Request body
application/json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"email": {
"type": "string"
},
"phone": {
"type": "string"
},
"code": {
"type": "string"
},
"for": {
"type": "string",
"enum": [
"read",
"write",
"identity",
"signup"
]
},
"user": {
"type": "string"
},
"scope": {
"type": "object",
"properties": {
"trip": {
"type": "string"
}
},
"required": [
"trip"
],
"additionalProperties": false
}
},
"required": [
"code",
"for"
],
"additionalProperties": false
}Responses
- 200
cookie response for "read"/"identity", token response for "write"/"signup"
- 400
invalid_request- 401
invalid_code— wrong, expired, burned, or the wrongfor- 403
signup_not_invited— the code is not spent- 404
signup_disabled;auth_disabled- 429
too_many_requests
/api/auth/links/redeem
postSpend a one-click sign-in link.
Request body
application/json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"token": {
"type": "string"
},
"for": {
"type": "string",
"enum": [
"read",
"identity"
]
},
"user": {
"type": "string"
}
},
"required": [
"token",
"for"
],
"additionalProperties": false
}Responses
- 200
the cookie is set on the response;
nextis where to land- 400
invalid_request- 401
link_spent— never followed, already spent, or expired — one answer for all three- 404
not_found— no such journal, or authentication is off- 429
too_many_requests
/api/auth/signup/phone
postProve a telephone number, step one.
Request body
application/json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"tel": {
"type": "string"
},
"channel": {
"type": "string",
"const": "sms"
}
},
"additionalProperties": false
}Responses
- 202
code mode: an id to redeem against. Inbound mode: a wa.me link and its prefilled text
- 400
invalid_request— tel missing or not a number with a country code;sms_unreachable— this server's number cannot reach that number's country- 401
invalid_token— missing, invalid, or not step two of signup- 403
signup_not_invited- 404
signup_disabled;sms_disabled- 429
too_many_requests— 3/number/day, 5/address/day, 50/instance/day- 503
verification_failed
/api/auth/signup/phone/redeem
postProve a telephone number, step two.
Request body
application/json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"id": {
"type": "string"
},
"code": {
"type": "string"
}
},
"required": [
"id"
],
"additionalProperties": false
}Responses
- 200
proven, or the poll status
- 400
invalid_request— no id sent- 401
invalid_token;invalid_code- 403
signup_not_invited- 404
signup_disabled- 429
too_many_requests
/api/auth/handover
postSpend a handover credential for your own 7-day token.
Responses
- 200
a 7-day agent token, and the status URL to read next
- 401
missing_token;invalid_handover— expired, already used, revoked, or not a handover credential- 404
auth_disabled
/api/auth/{user}/handover
postIssue a handover credential the owner can paste into an agent — owner only.
Responses
- 200
a 20-minute credential that can only be exchanged, never used to read or write
- 403
forbidden— a trip-scoped bearer, or a caller that is not this journal's owner- 404
auth_disabled- 409
no_owner_address
/api/auth/{user}/gps-token
postMint the owner's write:gps token — owner cookie only, refuses even the owner's own agent bearer.
Responses
- 200
a 30-day token good for POST /api/v2/{user}/import with kind "gps" and dryRun false, and nothing else
- 403
forbidden— not signed in as this journal's owner in a browser — a bearer token never counts here- 404
auth_disabled- 409
no_owner_address
/api/auth/{user}/keys
getThe tokens and sessions that can write here — the owner sees every row, anybody else only their own.
Responses
- 200
one row per live credential this caller may see
- 403
forbidden— no proven address at all- 409
auth_disabled
postRevoke one of them — the owner may revoke any row, anybody else only their own.
Request body
application/json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"revoke": {
"type": "string"
}
},
"required": [
"revoke"
],
"additionalProperties": false
}Responses
- 200
revoked
- 400
invalid_request— no key id sent- 403
forbidden— no proven address at all- 404
unknown_key— does not exist, or — for a non-owner — belongs to somebody else's address- 409
auth_disabled