/documentation.txt · /api/v2/openapi.json
Fernscout — 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://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=. A name that is a folder on disk or belonged to a deleted journal reads as username_taken, never as a deletion.
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/person
getThe person signed in on this browser across the instance (B-3007): their address and proved number, paired, and their saved postal address. fs_identity cookie only.
Responses
- 200
the person
- 401
not_signed_in- 404
auth_disabled
putSave the person's postal address once for the instance. It replaces the address in every journal that holds the person's own (never one the owner typed) and is offered when they join another. Cookie only.
Request body
application/json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"address": {
"type": "object",
"properties": {
"name": {
"type": "string",
"maxLength": 120,
"description": "who the envelope is addressed to"
},
"line1": {
"type": "string",
"maxLength": 120,
"description": "trimmed; longer is cut at 120 characters"
},
"line2": {
"type": "string",
"maxLength": 120,
"description": "trimmed; longer is cut at 120 characters"
},
"postcode": {
"type": "string",
"maxLength": 120,
"description": "trimmed; longer is cut at 120 characters"
},
"city": {
"type": "string",
"maxLength": 120,
"description": "trimmed; longer is cut at 120 characters"
},
"country": {
"type": "string",
"maxLength": 120,
"description": "trimmed; longer is cut at 120 characters"
}
},
"additionalProperties": false,
"description": "a street (line1), city and country are required; it replaces the address in every journal holding the person's own. Every field empty forgets the saved address and changes no journal."
}
},
"required": [
"address"
],
"additionalProperties": false
}Responses
- 200
saved; how many journals carry it
- 400
invalid_request- 401
not_signed_in- 403
foreign_origin- 404
auth_disabled;contacts_disabled— no encryption key, so no postal address can be kept- 413
body_too_large— the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status
/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/client-error
postReport a crash from the browser or the iOS shell. No auth. Written to the server log only, never stored; 30 per 15 minutes per client address. Caps: message 500, stack 4000, route 200, appVersion 40, requestId and digest 40 characters of [A-Za-z0-9_-].
Request body
application/json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"message": {
"type": "string",
"maxLength": 500
},
"stack": {
"type": "string",
"maxLength": 4000
},
"route": {
"type": "string",
"maxLength": 200
},
"appVersion": {
"type": "string",
"maxLength": 40
},
"platform": {
"type": "string",
"enum": [
"web",
"ios"
]
},
"requestId": {
"type": "string",
"maxLength": 40,
"pattern": "^[A-Za-z0-9_-]+$"
},
"digest": {
"type": "string",
"maxLength": 40,
"pattern": "^[A-Za-z0-9_-]+$"
}
},
"required": [
"message",
"route",
"appVersion",
"platform"
],
"additionalProperties": false
}Responses
- 204
logged
- 400
invalid_request;invalid_json- 413
body_too_large- 429
too_many_requests
/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 and what could be reclaimed
- 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), a phone's address book (`kind: "contacts"`, read and reported — nothing is written until the agreed rows are sent to POST .../contacts/import), or one trip from a Polarsteps export (`kind: "polarsteps"`, `text` is one trip.json — a draft trip and one draft day per local date; `locations.json` from the same export goes through this door too, as `kind: "gps"`, never through `polarsteps`). Send ?dryRun to preview. JSON `{kind, format?, inbox|text}`, or multipart with `file`. The owner's response also carries `coverage` (B2303): per trip, how many of its days the upload touches and `daysWithoutPlace`, the bare dates of its existing days that name no place; never a place name, and nothing is filled. 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",
"polarsteps"
]
},
"format": {
"type": "string"
},
"inbox": {
"type": "string"
},
"text": {
"type": "string"
},
"state": {
"type": "object",
"properties": {
"trip": {
"type": "string",
"minLength": 1
},
"armed": {
"type": "boolean"
},
"armedUntil": {
"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))$"
},
"permission": {
"type": "string",
"enum": [
"always",
"whenInUse",
"denied",
"notDetermined"
]
},
"appVersion": {
"type": "string",
"maxLength": 40
}
},
"required": [
"trip",
"armed"
],
"additionalProperties": false
}
},
"required": [
"kind"
],
"additionalProperties": false
}Responses
- 200
a gps import, stored (or previewed under ?dryRun) — a contacts import, read and reported, never written — or a polarsteps import, a draft trip and days (or previewed under ?dryRun)
- 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}/import/photos
getThe limits on handing photographs over, and the runs already holding some — owner only, and absent (404) when the guided import is off.
Responses
- 200
limits, staged bytes, and each run's photograph count
- 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
postStage photographs for the studio's guided import (multipart `file`, at most `maxFilesPerRequest` per call; `run` adds to an earlier run, absent opens a new one — no files at all just opens it). Trip and day are declined: nothing is filed, the owner sorts them into days in the studio, and staged photographs clear after two days unused. Owner token only — a `write:gps` or trip-scoped token is refused.
Request body
multipart/form-data
{
"type": "object",
"properties": {
"file": {
"type": "array",
"items": {
"type": "string",
"format": "binary"
}
},
"run": {
"type": "string",
"description": "a runId from an earlier call, to add to that run"
}
}
}Responses
- 200
what was staged and what was refused, with the run to resume
- 400
expected_multipart- 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- 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
/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- 422
stored_document_invalid— a stored day breaks the schema; details.problems names the day and field
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"
],
"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
},
"region": {
"type": "string"
},
"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"
},
"language": {
"type": "string",
"enum": [
"en",
"de",
"hu",
"fr",
"it"
]
},
"ownWords": {
"type": "string"
}
},
"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
},
"guestsLive": {
"type": "boolean"
},
"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;stored_document_invalid— a stored day breaks the schema; details.problems names the day and field
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"
],
"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
},
"region": {
"type": "string"
},
"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"
},
"language": {
"type": "string",
"enum": [
"en",
"de",
"hu",
"fr",
"it"
]
},
"ownWords": {
"type": "string"
}
},
"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
},
"guestsLive": {
"type": "boolean"
},
"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;stored_document_invalid— a stored day breaks the schema; details.problems names the day and field
/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
},
"region": {
"type": "string"
},
"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"
},
"language": {
"type": "string",
"enum": [
"en",
"de",
"hu",
"fr",
"it"
]
},
"ownWords": {
"type": "string"
}
},
"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
},
"region": {
"type": "string"
},
"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"
},
"language": {
"type": "string",
"enum": [
"en",
"de",
"hu",
"fr",
"it"
]
},
"ownWords": {
"type": "string"
}
},
"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- 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- 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 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). Zones are optional; the recorder never waits for one (B2568).
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
}
}
},
"required": [
"zones"
],
"additionalProperties": false
}Responses
- 200
the zones 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}/trips/{trip}/track-edits
getOne trip's hidden spots, hidden stretches and named stretches — what a reader is never shown of this trip's route, and what a stretch was called instead. Hiding never deletes the owner's own recorded positions; it only changes what a reader is ever shown of them.
Responses
- 200
the spots and stretches, 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;unknown_trip
putReplace the whole list of hidden spots, hidden stretches and named stretches for this trip. Requires If-Match with the ETag GET last answered — refused without one, or with a stale one, as stale_document (409). Re-derives this trip's track.json and track-recent.json immediately, so a hidden spot cuts the reader-facing line off from the moment it is saved.
Request body
application/json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"hiddenSpots": {
"maxItems": 20,
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"minLength": 1,
"maxLength": 64,
"pattern": "^[A-Za-z0-9_-]+$"
},
"lat": {
"type": "number",
"minimum": -90,
"maximum": 90
},
"lon": {
"type": "number",
"minimum": -180,
"maximum": 180
},
"radiusM": {
"type": "number",
"minimum": 50,
"maximum": 5000
}
},
"required": [
"lat",
"lon",
"radiusM"
],
"additionalProperties": false
}
},
"hiddenStretches": {
"maxItems": 20,
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"minLength": 1,
"maxLength": 64,
"pattern": "^[A-Za-z0-9_-]+$"
},
"date": {
"type": "string",
"pattern": "^\\d{4}-\\d{2}-\\d{2}$"
},
"from": {
"type": "string",
"pattern": "^([01]\\d|2[0-3]):[0-5]\\d$"
},
"to": {
"type": "string",
"pattern": "^([01]\\d|2[0-3]):[0-5]\\d$"
}
},
"required": [
"date",
"from",
"to"
],
"additionalProperties": false
}
},
"namedStretches": {
"maxItems": 20,
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"minLength": 1,
"maxLength": 64,
"pattern": "^[A-Za-z0-9_-]+$"
},
"date": {
"type": "string",
"pattern": "^\\d{4}-\\d{2}-\\d{2}$"
},
"from": {
"type": "string",
"pattern": "^([01]\\d|2[0-3]):[0-5]\\d$"
},
"to": {
"type": "string",
"pattern": "^([01]\\d|2[0-3]):[0-5]\\d$"
},
"label": {
"type": "string",
"minLength": 1,
"maxLength": 80
},
"mode": {
"type": "string",
"enum": [
"on_foot",
"bike",
"car",
"bus",
"train",
"tram",
"boat",
"plane",
"skiing",
"unknown"
]
}
},
"required": [
"date",
"from",
"to",
"label"
],
"additionalProperties": false
}
}
},
"required": [
"hiddenSpots",
"hiddenStretches",
"namedStretches"
],
"additionalProperties": false
}Responses
- 200
the spots and stretches, with ids assigned and 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;unknown_trip- 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
/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}/visited
getThe countries recorded without a trip. The owner's token sees every entry; any other token sees only `public` ones.
Responses
- 200
the entries this token may see
- 401
missing_token;invalid_token- 403
out_of_scope— the token is for a different journal or trip- 404
no_such_journal
postAdd one country (`{country, places?, year?, month?, note?, visibility?}`) or many (`{entries: [...]}`). A country that already has an entry answers with that entry unchanged (200, or absent from `created` in a batch) — never a duplicate. Owner only.
Request body
application/json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"anyOf": [
{
"type": "object",
"properties": {
"country": {
"type": "string",
"minLength": 2,
"maxLength": 2
},
"places": {
"type": "string",
"minLength": 1,
"maxLength": 200
},
"year": {
"type": "integer",
"minimum": 1900,
"maximum": 9007199254740991
},
"month": {
"type": "integer",
"minimum": 1,
"maximum": 12
},
"note": {
"type": "string",
"minLength": 1,
"maxLength": 1000
},
"visibility": {
"type": "string",
"enum": [
"private",
"public",
"guest"
]
}
},
"required": [
"country"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"entries": {
"minItems": 1,
"maxItems": 300,
"type": "array",
"items": {
"type": "object",
"properties": {
"country": {
"type": "string",
"minLength": 2,
"maxLength": 2
},
"places": {
"type": "string",
"minLength": 1,
"maxLength": 200
},
"year": {
"type": "integer",
"minimum": 1900,
"maximum": 9007199254740991
},
"month": {
"type": "integer",
"minimum": 1,
"maximum": 12
},
"note": {
"type": "string",
"minLength": 1,
"maxLength": 1000
},
"visibility": {
"type": "string",
"enum": [
"private",
"public",
"guest"
]
}
},
"required": [
"country"
],
"additionalProperties": false
}
}
},
"required": [
"entries"
],
"additionalProperties": false
}
]
}Responses
- 200
a batch: every entry as it stands, and which codes were newly added
- 201
added (single)
- 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- 413
body_too_large— the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status
/api/v2/{user}/visited/{code}
getOne country, by its ISO 3166-1 alpha-2 code.
Responses
- 200
the entry
- 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;not_found
patchChange an entry's places, year, month, note or visibility (`null` clears a field; the photograph has its own call). Owner only.
Request body
application/json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"places": {
"anyOf": [
{
"type": "string",
"minLength": 1,
"maxLength": 200
},
{
"type": "null"
}
]
},
"year": {
"anyOf": [
{
"type": "integer",
"minimum": 1900,
"maximum": 9007199254740991
},
{
"type": "null"
}
]
},
"month": {
"anyOf": [
{
"type": "integer",
"minimum": 1,
"maximum": 12
},
{
"type": "null"
}
]
},
"note": {
"anyOf": [
{
"type": "string",
"minLength": 1,
"maxLength": 1000
},
{
"type": "null"
}
]
},
"visibility": {
"type": "string",
"enum": [
"private",
"public",
"guest"
]
}
},
"additionalProperties": false
}Responses
- 200
the entry as it now stands
- 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;not_found- 413
body_too_large— the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status
deleteRemove an entry and its served photograph; the photograph's original is kept. Owner only.
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
no_such_journal;not_found
/api/v2/{user}/visited/{code}/photo
deleteDetach the entry's photograph (the original is kept). Owner only.
Responses
- 200
the entry without a photo
- 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;not_found
putSet the entry's one photograph (replacing any) through the ordinary image ingest — the original is kept as the print master. Owner only.
Request body
multipart/form-data
{
"type": "object",
"properties": {
"file": {
"type": "string",
"format": "binary"
}
},
"required": [
"file"
]
}Responses
- 200
the entry, with its
photo- 400
invalid_request;expected_multipart;invalid_media;storage_full- 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- 413
body_too_large
/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
plan_limit- 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}/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"
},
"link": {
"type": "boolean"
}
},
"required": [
"for"
],
"additionalProperties": false
}Responses
- 202
accepted — always, whatever the address
- 400
invalid_request— a shape rule, never address-dependent;invalid_email;sms_unreachable— phone: this server does not text that country; nothing sent- 403
not_authorised— for: "write" to an address that owns nothing and is on no named trip- 404
signup_disabled;auth_disabled- 429
too_many_requests— narrower for write than for read- 503
sms_disabled— phone: this server cannot send SMS; nothing issued;mail_disabled— nothing issued; any code already held is still live;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
},
"link": {
"type": "boolean"
},
"replace": {
"type": "boolean"
}
},
"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, the wrongfor, or (signup) an address not on an invite-only list- 404
signup_disabled;auth_disabled- 409
too_many_journals— for "signup": this address already keeps a journal — the code proved it, so the identity cookie is set anddetails.usernames that address's own journal (nobody else's)- 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- 409
tel_taken— the proven number already keeps a journal on this server- 429
too_many_requests
/api/auth/signup/state
getWhere this signup stands. The signup token from codes/redeem rides as `Authorization: Bearer`. A signup that stopped after the address or the number picks up here: the address proof and a number proven within the last seven days (on any device) survive for eight days, and `phoneProven` says whether this token already carries one. `telMasked` shows only the last two digits; `phoneRequired` is false for the operator's own address.
Responses
- 200
the open and the proven steps
- 401
invalid_token- 403
signup_not_invited- 404
signup_disabled
/api/auth/signup/identity
postA signup token for the address this browser's identity cookie proves — cookie only, no code, no body.
Responses
- 200
the same token codes/redeem returns for "signup" — use it as for that one
- 401
not_signed_in— no identity cookie, or it names a phone number rather than an address- 403
signup_not_invited;foreign_origin- 404
signup_disabled- 409
too_many_journals- 429
too_many_requests
/api/auth/signup/resume
postSpend the signup code mail's "Continue my signup" link for a signup token — single use, 7 days, no cookie set.
Request body
application/json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"token": {
"type": "string",
"minLength": 1
}
},
"required": [
"token"
],
"additionalProperties": false
}Responses
- 200
the same token codes/redeem returns for "signup" — in the body only, no cookie;
resumedis true when a pending signup for the address existed before this press (copy only)- 400
invalid_request- 401
invalid_resume_link— spent, expired (7 days) or unknown- 403
signup_not_invited;foreign_origin- 404
signup_disabled- 409
too_many_journals- 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