/documentation.txt · /api/v2/openapi.json

Fernscout dev — v2 API

Generated from the frozen Zod contract in lib/api/v2/schemas/ — every field, enum and required/optional marker here is what a validator actually checks, not a second hand-typed copy of it. Every path below is relative to https://dev.fernscout.ch, and every "{user}" is a journal's username (e.g. "example").

/api/v2/openapi.json

getThis document. No auth.

Responses

200

the v2 OpenAPI 3.1 document

405

a verb this route does not answer

/api/v2/status

getWhat this instance can do, and what things cost. No auth.

Responses

200

the instance's capabilities, limits and pricing

405

a verb this route does not answer

/api/v2/journals/available

getWhether a username is free to sign up with — send ?username=.

Responses

200

always 200 — availability is reported in the body, not the status

400

invalid_request — no ?username= given

/api/v2/journals

postCreate a journal, spending a signup token minted by POST /api/auth/codes.

Request body

application/json

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "username": {
      "type": "string",
      "minLength": 1
    },
    "title": {
      "type": "string",
      "minLength": 1,
      "maxLength": 200
    },
    "ownerName": {
      "type": "string",
      "minLength": 1
    },
    "ownerNickname": {
      "type": "string",
      "minLength": 1
    },
    "tagline": {
      "type": "string",
      "minLength": 1
    },
    "visibility": {
      "type": "string"
    },
    "defaultLocale": {
      "type": "string"
    },
    "locales": {
      "type": "array",
      "items": {
        "type": "string"
      }
    },
    "baseCurrency": {
      "type": "string"
    },
    "displayCurrencies": {
      "type": "array",
      "items": {
        "type": "string"
      }
    },
    "units": {
      "type": "string",
      "enum": [
        "metric",
        "imperial"
      ]
    },
    "tips": {
      "type": "boolean"
    }
  },
  "required": [
    "username",
    "title",
    "ownerName",
    "ownerNickname"
  ],
  "additionalProperties": false
}

Responses

201

the journal exists; a one-time sign-in link and the agent token ride along

400

invalid_request; phone_required; invalid_username; invalid_title; invalid_owner

401

missing_token; invalid_token

403

signup_not_invited — this instance is invite-only and the address is not on its list; reserved_username; too_many_journals

404

signup_disabled

409

username_taken; tel_taken

410

deleted_username

413

body_too_large — the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status

429

too_many_requests — per-IP creation/refusal rate limit

/api/v2/me/home

getWhat this address may open, and its own device list. No bearer token — an fs_identity cookie, or nobody.

Responses

200

always 200 — id: null is a stranger, not a refusal

405

a verb this route does not answer

/api/v2/me/devices

deleteSign out everywhere: end every browser sign-in this address holds — each device's identity and each journal's reader cookie. Agent keys and GPS tokens are not touched. Browser cookie only.

Responses

200

how many sign-ins ended; this browser's cookies are cleared too

401

not_signed_in

403

foreign_origin

404

auth_disabled

/api/v2/me/devices/{id}

deleteEnd one device's identity. The id is checked against this address's own list.

Responses

200

revoked

401

not_signed_in

404

auth_disabled; no_such_device

/api/v2/geocode

postA place name into candidate coordinates, never a guess.

Request body

application/json

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "query": {
      "type": "string",
      "minLength": 1
    },
    "countryHint": {
      "type": "string"
    },
    "regionHint": {
      "type": "string"
    },
    "contextCoordinates": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "lat": {
            "type": "number",
            "minimum": -90,
            "maximum": 90
          },
          "lng": {
            "type": "number",
            "minimum": -180,
            "maximum": 180
          }
        },
        "required": [
          "lat",
          "lng"
        ],
        "additionalProperties": false
      }
    }
  },
  "required": [
    "query"
  ],
  "additionalProperties": false
}

Responses

200

a ranked shortlist, possibly empty

400

invalid_request

401

missing_token; invalid_token

404

address_lookup_disabled

413

body_too_large — the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status

429

too_many_requests

502

provider_unavailable

/api/v2/{user}

getThe journal document.

Responses

200

the stored journal, with its ETag

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

no_such_journal

patchMerge-patch the journal document. A CHANGED owner.email does not write — it starts a verification at `.../owner/email/redeem` instead, and answers 202.

Request body

application/json

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "title": {
      "type": "string",
      "minLength": 1,
      "maxLength": 200
    },
    "owner": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string",
          "minLength": 1
        },
        "nickname": {
          "type": "string",
          "minLength": 1
        },
        "email": {
          "type": "string",
          "format": "email",
          "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
        }
      },
      "required": [
        "name",
        "nickname",
        "email"
      ],
      "additionalProperties": false
    },
    "locales": {
      "minItems": 1,
      "type": "array",
      "items": {
        "type": "string"
      }
    },
    "baseCurrency": {
      "type": "string",
      "minLength": 3,
      "maxLength": 3
    },
    "displayCurrencies": {
      "minItems": 1,
      "type": "array",
      "items": {
        "type": "string",
        "minLength": 3,
        "maxLength": 3
      }
    },
    "units": {
      "type": "string",
      "enum": [
        "metric",
        "imperial"
      ]
    },
    "visibility": {
      "type": "string",
      "enum": [
        "public",
        "guest"
      ]
    },
    "tagline": {
      "type": "string"
    },
    "figures": {
      "anyOf": [
        {
          "type": "object",
          "properties": {
            "mode": {
              "type": "string",
              "const": "off"
            }
          },
          "required": [
            "mode"
          ],
          "additionalProperties": false
        },
        {
          "type": "object",
          "properties": {
            "mode": {
              "type": "string",
              "const": "set"
            },
            "figures": {
              "minItems": 1,
              "maxItems": 10,
              "type": "array",
              "items": {
                "type": "string",
                "minLength": 1
              }
            }
          },
          "required": [
            "mode",
            "figures"
          ],
          "additionalProperties": false
        }
      ]
    },
    "declined": {
      "type": "object",
      "propertyNames": {
        "type": "string",
        "enum": [
          "tagline",
          "figures"
        ]
      },
      "additionalProperties": {
        "type": "string",
        "minLength": 10
      }
    }
  },
  "additionalProperties": false
}

Responses

200

the merged, re-validated document

202

owner.email changed to a different, valid address — a code went to it, and nothing was written

400

invalid_request

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

no_such_journal

409

stale_document — If-Match did not cover the current ETag

413

body_too_large — the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status

422

incomplete — the merged document leaves a required section neither answered nor declined

429

too_many_requests — too many owner-email codes for this address or this journal today

503

mail_disabled — owner.email changed, but this server cannot send the verification code; mail_failed — owner.email changed, but the verification code could not be sent

deleteAsk to delete the journal — mails the owner a single-use confirmation link. Deletes nothing itself.

Responses

202

a mail is on its way; nothing has been deleted

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

no_such_journal

409

mail_disabled

410

gone — already deleted

429

too_many_requests — too many deletion mails to this address recently

/api/v2/{user}/status

getWhere this journal and this token stand: drafts, trips, storage, inbox.

Responses

200

the journal's live status

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip

404

no_such_journal

/api/v2/{user}/storage

getWhere this journal's storage is going.

Responses

200

usage, breakdown, what could be reclaimed, and the price of more

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip

404

no_such_journal

/api/v2/{user}/sync/manifest

getEvery file this journal's folder holds, with a hash — the up-to-date check for a local mirror, and a complete one: the full-resolution originals are included, so a first pull is large and a later one carries only what the hashes say changed. This journal's position history is in no manifest and behind no route. Owner only.

Responses

200

path, size and hash of every syncable file

401

missing_token; invalid_token

404

not_found — the same refusal whether the journal is unknown, the token is for a different one, or it is merely trip-scoped

/api/v2/{user}/sync/file/{path}

getOne file's bytes, by the path the manifest named. Read-only, owner only.

Responses

200

the file's bytes, with a content-type guessed from its name

401

missing_token; invalid_token

404

not_found — the same refusal whether the journal is unknown, the token is for a different one, or it is merely trip-scoped

/api/v2/{user}/import

getThe kinds and formats this door reads, and the ceiling on a request's size. Owner only.

Responses

200

kinds, formats and the byte ceiling

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

no_such_journal

postImport a location history (`kind: "gps"`, stored as read) or a phone's address book (`kind: "contacts"`, read and reported — nothing is written until the agreed rows are sent to POST .../contacts/import). Send ?dryRun to preview. JSON `{kind, format?, inbox|text}`, or multipart with `file`. Owner only, with one exception (B2204): a `write:gps` token — minted at POST /api/auth/{user}/gps-token — may call this too, and only this, and only for `kind: "gps"` with `dryRun` false or absent; its response has no `extent`. Everything else about this door refuses that token exactly as it refuses any other non-owner call.

Request body

application/json

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "kind": {
      "type": "string",
      "enum": [
        "gps",
        "contacts"
      ]
    },
    "format": {
      "type": "string"
    },
    "inbox": {
      "type": "string"
    },
    "text": {
      "type": "string"
    }
  },
  "required": [
    "kind"
  ],
  "additionalProperties": false
}

Responses

200

a gps import, stored (or previewed under ?dryRun) — or a contacts import, read and reported, never written

400

invalid_request; expected_file; invalid_body; no_file; unknown_kind; storage_full; unreadable; contract

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

no_such_journal; unknown_user; unknown_inbox_file

413

body_too_large

/api/v2/{user}/gps

getWhich months of raw location history this journal holds — names, never a fix. Owner only.

Responses

200

YYYY-MM, sorted

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

no_such_journal

/api/v2/{user}/channels

getThe owner's own mute switches for the sending channels.

Responses

200

null for a channel this instance does not offer at all

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

no_such_journal

patchSwitch mail and/or WhatsApp sending on or off for this journal.

Request body

application/json

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "mail": {
      "type": "boolean"
    },
    "whatsapp": {
      "type": "boolean"
    }
  },
  "additionalProperties": false
}

Responses

200

the channels as they now stand

400

invalid_request

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

no_such_journal

409

capability_unavailable

413

body_too_large — the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status

/api/v2/{user}/owner/tel

getThe owner's own telephone number — a notification channel, not the address that owns the journal.

Responses

200

null fields when there is no number on file

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

no_such_journal

deleteClear the owner's own number — turns their free WhatsApp copy of a day back off.

Responses

200

tel: null

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

no_such_journal

/api/v2/{user}/owner/tel/verify

postStart proving a number for the owner's own telephone field — sends a one-time code.

Request body

application/json

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "tel": {
      "type": "string",
      "minLength": 1
    }
  },
  "required": [
    "tel"
  ],
  "additionalProperties": false
}

Responses

202

an opaque id — bring it, with the code, to .../verify/redeem

400

invalid_request

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

no_such_journal

409

capability_unavailable

413

body_too_large — the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status

429

too_many_requests

503

verification_failed

/api/v2/{user}/owner/tel/verify/redeem

postFinish proving a number — the code from `.../verify` writes owner.tel, proven, for good.

Request body

application/json

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "id": {
      "type": "string",
      "minLength": 1
    },
    "code": {
      "type": "string",
      "minLength": 1
    }
  },
  "required": [
    "id",
    "code"
  ],
  "additionalProperties": false
}

Responses

200

the number, now proven

400

invalid_request

401

missing_token; invalid_token; invalid_code

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

no_such_journal

413

body_too_large — the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status

429

too_many_requests

/api/v2/{user}/owner/email/redeem

postFinish moving owner.email — the code the new address received writes it, revokes every session and agent token the old address held for this journal, and mails the old address.

Request body

application/json

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "id": {
      "type": "string",
      "minLength": 1
    },
    "code": {
      "type": "string",
      "minLength": 1
    }
  },
  "required": [
    "id",
    "code"
  ],
  "additionalProperties": false
}

Responses

200

the journal document, with owner.email now moved

400

invalid_request

401

missing_token; invalid_token; invalid_code

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

no_such_journal

413

body_too_large — the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status

429

too_many_requests

/api/v2/{user}/trips

getEvery trip this token may write, paged.

Responses

200

one page of trip documents

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip

404

no_such_journal

/api/v2/{user}/trips/{trip}

getOne trip. `?days=full|summaries|none` controls how much of each day rides along (default full, dayDoc[]); summaries and none change the shape of `days` away from what `tripDoc` documents for a plain GET.

Responses

200

the trip, with its days inline (mode full)

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip

404

no_such_journal; unknown_trip

patchMerge-patch a trip.

Request body

application/json

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "id": {
      "type": "string",
      "pattern": "^[a-z0-9][a-z0-9-]*$"
    },
    "title": {
      "type": "string",
      "minLength": 1,
      "maxLength": 200
    },
    "dates": {
      "type": "object",
      "properties": {
        "from": {
          "type": "string",
          "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
        },
        "to": {
          "type": "string",
          "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
        }
      },
      "required": [
        "from",
        "to"
      ],
      "additionalProperties": false
    },
    "visibility": {
      "type": "string",
      "enum": [
        "private",
        "public",
        "guest"
      ]
    },
    "people": {
      "minItems": 1,
      "maxItems": 10,
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1
          },
          "nickname": {
            "type": "string",
            "minLength": 1
          },
          "email": {
            "type": "string",
            "format": "email",
            "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
          }
        },
        "required": [
          "name",
          "email"
        ],
        "additionalProperties": false
      }
    },
    "teaser": {
      "type": "boolean"
    },
    "rates": {
      "type": "object",
      "properties": {
        "currencies": {
          "minItems": 1,
          "type": "array",
          "items": {
            "type": "string",
            "minLength": 3,
            "maxLength": 3
          }
        },
        "manual": {
          "type": "object",
          "propertyNames": {
            "type": "string",
            "minLength": 3,
            "maxLength": 3
          },
          "additionalProperties": {
            "type": "number",
            "exclusiveMinimum": 0
          }
        }
      },
      "required": [
        "currencies"
      ],
      "additionalProperties": false
    },
    "costs": {
      "type": "object",
      "properties": {
        "budget": {
          "type": "object",
          "properties": {
            "total": {
              "type": "number",
              "exclusiveMinimum": 0
            },
            "days": {
              "type": "integer",
              "exclusiveMinimum": 0,
              "maximum": 9007199254740991
            },
            "currency": {
              "type": "string",
              "minLength": 3,
              "maxLength": 3
            }
          },
          "required": [
            "total"
          ],
          "additionalProperties": false
        },
        "items": {
          "maxItems": 200,
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "label": {
                "type": "string",
                "minLength": 1,
                "maxLength": 200
              },
              "amount": {
                "type": "number",
                "exclusiveMinimum": 0
              },
              "category": {
                "type": "string",
                "enum": [
                  "preparation",
                  "flights",
                  "accommodation",
                  "food",
                  "transport",
                  "activities",
                  "other"
                ]
              },
              "currency": {
                "type": "string",
                "minLength": 3,
                "maxLength": 3
              },
              "stop": {
                "type": "string"
              }
            },
            "required": [
              "label",
              "amount"
            ],
            "additionalProperties": false
          }
        },
        "note": {
          "type": "string"
        },
        "visibility": {
          "type": "string",
          "enum": [
            "public",
            "guests"
          ]
        }
      },
      "additionalProperties": false
    },
    "plan": {
      "type": "object",
      "properties": {
        "route": {
          "minItems": 1,
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "pattern": "^[a-z0-9][a-z0-9-]*$"
              },
              "location": {
                "type": "string",
                "minLength": 1
              },
              "lat": {
                "type": "number",
                "minimum": -90,
                "maximum": 90
              },
              "lng": {
                "type": "number",
                "minimum": -180,
                "maximum": 180
              },
              "country": {
                "type": "string"
              },
              "countryCode": {
                "type": "string",
                "minLength": 2,
                "maxLength": 2
              },
              "note": {
                "type": "string"
              },
              "nights": {
                "type": "integer",
                "exclusiveMinimum": 0,
                "maximum": 9007199254740991
              },
              "arrive": {
                "type": "string",
                "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
              },
              "leave": {
                "type": "string",
                "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
              },
              "see": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "name": {
                      "type": "string",
                      "minLength": 1
                    },
                    "lat": {
                      "type": "number",
                      "minimum": -90,
                      "maximum": 90
                    },
                    "lng": {
                      "type": "number",
                      "minimum": -180,
                      "maximum": 180
                    },
                    "source": {
                      "type": "string",
                      "enum": [
                        "typed",
                        "maps-link",
                        "pin",
                        "coordinates"
                      ]
                    }
                  },
                  "required": [
                    "name",
                    "lat",
                    "lng"
                  ],
                  "additionalProperties": false
                }
              },
              "source": {
                "type": "string",
                "enum": [
                  "typed",
                  "maps-link",
                  "pin",
                  "coordinates"
                ]
              }
            },
            "required": [
              "location",
              "lat",
              "lng"
            ],
            "additionalProperties": false
          }
        },
        "body": {
          "type": "string"
        },
        "mode": {
          "type": "string",
          "enum": [
            "nights",
            "dates"
          ]
        },
        "readers": {
          "type": "string",
          "enum": [
            "map",
            "details"
          ]
        },
        "private": {
          "type": "object",
          "properties": {
            "links": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "label": {
                    "type": "string",
                    "minLength": 1
                  },
                  "url": {
                    "type": "string",
                    "format": "uri"
                  }
                },
                "required": [
                  "label",
                  "url"
                ],
                "additionalProperties": false
              }
            },
            "stops": {
              "type": "object",
              "propertyNames": {
                "type": "string"
              },
              "additionalProperties": {
                "type": "object",
                "properties": {
                  "stay": {
                    "type": "object",
                    "properties": {
                      "name": {
                        "type": "string",
                        "minLength": 1
                      },
                      "lat": {
                        "type": "number",
                        "minimum": -90,
                        "maximum": 90
                      },
                      "lng": {
                        "type": "number",
                        "minimum": -180,
                        "maximum": 180
                      }
                    },
                    "required": [
                      "name",
                      "lat",
                      "lng"
                    ],
                    "additionalProperties": false
                  },
                  "links": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "label": {
                          "type": "string",
                          "minLength": 1
                        },
                        "url": {
                          "type": "string",
                          "format": "uri"
                        }
                      },
                      "required": [
                        "label",
                        "url"
                      ],
                      "additionalProperties": false
                    }
                  }
                },
                "additionalProperties": false
              }
            }
          },
          "additionalProperties": false
        }
      },
      "required": [
        "route"
      ],
      "additionalProperties": false
    },
    "days": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}-[a-z0-9]+(-[a-z0-9]+)*$"
          },
          "title": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "date": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
          },
          "content": {
            "type": "string",
            "maxLength": 100000
          },
          "media": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "src": {
                  "type": "string"
                },
                "caption": {
                  "type": "string"
                },
                "visibility": {
                  "type": "string",
                  "enum": [
                    "guest",
                    "private"
                  ]
                }
              },
              "required": [
                "src"
              ],
              "additionalProperties": false
            }
          },
          "costs": {
            "maxItems": 200,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "label": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 200
                },
                "amount": {
                  "type": "number",
                  "exclusiveMinimum": 0
                },
                "category": {
                  "type": "string",
                  "enum": [
                    "preparation",
                    "flights",
                    "accommodation",
                    "food",
                    "transport",
                    "activities",
                    "other"
                  ]
                },
                "currency": {
                  "type": "string",
                  "minLength": 3,
                  "maxLength": 3
                },
                "stop": {
                  "type": "string"
                }
              },
              "required": [
                "label",
                "amount"
              ],
              "additionalProperties": false
            }
          },
          "coordinates": {
            "type": "object",
            "properties": {
              "lat": {
                "type": "number",
                "minimum": -90,
                "maximum": 90
              },
              "lng": {
                "type": "number",
                "minimum": -180,
                "maximum": 180
              }
            },
            "required": [
              "lat",
              "lng"
            ],
            "additionalProperties": false
          },
          "weather": {
            "anyOf": [
              {
                "type": "boolean",
                "const": true
              },
              {
                "type": "object",
                "properties": {
                  "tempMin": {
                    "type": "number",
                    "minimum": -90,
                    "maximum": 60
                  },
                  "tempMax": {
                    "type": "number",
                    "minimum": -90,
                    "maximum": 60
                  },
                  "code": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 99
                  },
                  "precipitation": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 2000
                  },
                  "windMax": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 500
                  },
                  "recordedAt": {
                    "type": "string",
                    "format": "date-time",
                    "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
                  },
                  "source": {
                    "type": "string",
                    "minLength": 1
                  }
                },
                "required": [
                  "recordedAt",
                  "source"
                ],
                "additionalProperties": false
              }
            ]
          },
          "time": {
            "type": "string",
            "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$"
          },
          "timezone": {
            "type": "string"
          },
          "location": {
            "type": "string"
          },
          "country": {
            "type": "string"
          },
          "countryCode": {
            "type": "string",
            "minLength": 2,
            "maxLength": 2
          },
          "transportMode": {
            "type": "string",
            "enum": [
              "flight",
              "train",
              "bus",
              "motorbike",
              "bicycle",
              "boat",
              "car",
              "taxi",
              "walk",
              "metro",
              "tram",
              "ferry"
            ]
          },
          "tags": {
            "maxItems": 10,
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 30,
              "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$"
            }
          },
          "translations": {
            "type": "object",
            "propertyNames": {
              "type": "string"
            },
            "additionalProperties": {
              "type": "object",
              "properties": {
                "title": {
                  "type": "string"
                },
                "content": {
                  "type": "string"
                }
              },
              "required": [
                "title",
                "content"
              ],
              "additionalProperties": false
            }
          },
          "visibility": {
            "type": "string",
            "enum": [
              "guest",
              "private"
            ]
          },
          "status": {
            "type": "string",
            "const": "draft"
          },
          "declined": {
            "type": "object",
            "propertyNames": {
              "type": "string",
              "enum": [
                "media",
                "costs",
                "coordinates",
                "weather",
                "time",
                "timezone",
                "location",
                "country",
                "countryCode",
                "transportMode",
                "tags",
                "translations",
                "visibility",
                "status"
              ]
            },
            "additionalProperties": {
              "type": "string",
              "minLength": 10
            }
          },
          "transportFrom": {
            "type": "string"
          },
          "transportTo": {
            "type": "string"
          },
          "travelScene": {
            "type": "string",
            "enum": [
              "default",
              "quick",
              "skip"
            ]
          },
          "test": {
            "type": "boolean"
          }
        },
        "required": [
          "slug",
          "title",
          "date",
          "content"
        ],
        "additionalProperties": false
      }
    },
    "translations": {
      "type": "object",
      "propertyNames": {
        "type": "string"
      },
      "additionalProperties": {
        "type": "object",
        "properties": {
          "title": {
            "type": "string"
          },
          "tagline": {
            "type": "string"
          },
          "intro": {
            "type": "string"
          }
        },
        "additionalProperties": false
      }
    },
    "accent": {
      "anyOf": [
        {
          "type": "string",
          "enum": [
            "sky",
            "yellow",
            "green",
            "coral",
            "navy"
          ]
        },
        {
          "type": "null"
        }
      ]
    },
    "cover": {
      "type": [
        "string",
        "null"
      ]
    },
    "figures": {
      "anyOf": [
        {
          "type": "object",
          "properties": {
            "mode": {
              "type": "string",
              "const": "off"
            }
          },
          "required": [
            "mode"
          ],
          "additionalProperties": false
        },
        {
          "type": "object",
          "properties": {
            "mode": {
              "type": "string",
              "const": "journal"
            }
          },
          "required": [
            "mode"
          ],
          "additionalProperties": false
        },
        {
          "type": "object",
          "properties": {
            "mode": {
              "type": "string",
              "const": "custom"
            },
            "figures": {
              "minItems": 1,
              "maxItems": 10,
              "type": "array",
              "items": {
                "type": "string",
                "minLength": 1
              }
            }
          },
          "required": [
            "mode",
            "figures"
          ],
          "additionalProperties": false
        }
      ]
    },
    "tagline": {
      "type": [
        "string",
        "null"
      ]
    },
    "intro": {
      "type": [
        "string",
        "null"
      ]
    },
    "listed": {
      "type": "boolean"
    },
    "reminder": {
      "type": "object",
      "properties": {
        "channel": {
          "type": "string",
          "enum": [
            "mail"
          ]
        }
      },
      "required": [
        "channel"
      ],
      "additionalProperties": false
    },
    "declined": {
      "type": "object",
      "propertyNames": {
        "type": "string",
        "enum": [
          "rates",
          "costs",
          "plan",
          "days",
          "translations",
          "accent",
          "cover",
          "figures",
          "tagline",
          "intro",
          "listed"
        ]
      },
      "additionalProperties": {
        "type": "string",
        "minLength": 10
      }
    },
    "test": {
      "type": "boolean"
    }
  },
  "additionalProperties": false
}

Responses

200

the merged, re-validated document

400

invalid_request; invalid_translations; invalid_cover

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

no_such_journal; unknown_trip

409

stale_document

413

body_too_large — the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status

422

incomplete

deleteAsk to delete a trip — mails the owner a single-use confirmation link. Deletes nothing itself.

Responses

202

a mail is on its way; nothing has been deleted

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

no_such_journal

410

gone

429

too_many_requests — too many deletion mails to this address recently

putCreate a trip at a client-chosen id, or replace one (with a matching If-Match).

Request body

application/json

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "id": {
      "type": "string",
      "pattern": "^[a-z0-9][a-z0-9-]*$"
    },
    "title": {
      "type": "string",
      "minLength": 1,
      "maxLength": 200
    },
    "dates": {
      "type": "object",
      "properties": {
        "from": {
          "type": "string",
          "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
        },
        "to": {
          "type": "string",
          "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
        }
      },
      "required": [
        "from",
        "to"
      ],
      "additionalProperties": false
    },
    "visibility": {
      "type": "string",
      "enum": [
        "private",
        "public",
        "guest"
      ]
    },
    "people": {
      "minItems": 1,
      "maxItems": 10,
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1
          },
          "nickname": {
            "type": "string",
            "minLength": 1
          },
          "email": {
            "type": "string",
            "format": "email",
            "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
          }
        },
        "required": [
          "name",
          "email"
        ],
        "additionalProperties": false
      }
    },
    "teaser": {
      "type": "boolean"
    },
    "rates": {
      "type": "object",
      "properties": {
        "currencies": {
          "minItems": 1,
          "type": "array",
          "items": {
            "type": "string",
            "minLength": 3,
            "maxLength": 3
          }
        },
        "manual": {
          "type": "object",
          "propertyNames": {
            "type": "string",
            "minLength": 3,
            "maxLength": 3
          },
          "additionalProperties": {
            "type": "number",
            "exclusiveMinimum": 0
          }
        }
      },
      "required": [
        "currencies"
      ],
      "additionalProperties": false
    },
    "costs": {
      "type": "object",
      "properties": {
        "budget": {
          "type": "object",
          "properties": {
            "total": {
              "type": "number",
              "exclusiveMinimum": 0
            },
            "days": {
              "type": "integer",
              "exclusiveMinimum": 0,
              "maximum": 9007199254740991
            },
            "currency": {
              "type": "string",
              "minLength": 3,
              "maxLength": 3
            }
          },
          "required": [
            "total"
          ],
          "additionalProperties": false
        },
        "items": {
          "maxItems": 200,
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "label": {
                "type": "string",
                "minLength": 1,
                "maxLength": 200
              },
              "amount": {
                "type": "number",
                "exclusiveMinimum": 0
              },
              "category": {
                "type": "string",
                "enum": [
                  "preparation",
                  "flights",
                  "accommodation",
                  "food",
                  "transport",
                  "activities",
                  "other"
                ]
              },
              "currency": {
                "type": "string",
                "minLength": 3,
                "maxLength": 3
              },
              "stop": {
                "type": "string"
              }
            },
            "required": [
              "label",
              "amount"
            ],
            "additionalProperties": false
          }
        },
        "note": {
          "type": "string"
        },
        "visibility": {
          "type": "string",
          "enum": [
            "public",
            "guests"
          ]
        }
      },
      "additionalProperties": false
    },
    "plan": {
      "type": "object",
      "properties": {
        "route": {
          "minItems": 1,
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "pattern": "^[a-z0-9][a-z0-9-]*$"
              },
              "location": {
                "type": "string",
                "minLength": 1
              },
              "lat": {
                "type": "number",
                "minimum": -90,
                "maximum": 90
              },
              "lng": {
                "type": "number",
                "minimum": -180,
                "maximum": 180
              },
              "country": {
                "type": "string"
              },
              "countryCode": {
                "type": "string",
                "minLength": 2,
                "maxLength": 2
              },
              "note": {
                "type": "string"
              },
              "nights": {
                "type": "integer",
                "exclusiveMinimum": 0,
                "maximum": 9007199254740991
              },
              "arrive": {
                "type": "string",
                "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
              },
              "leave": {
                "type": "string",
                "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
              },
              "see": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "name": {
                      "type": "string",
                      "minLength": 1
                    },
                    "lat": {
                      "type": "number",
                      "minimum": -90,
                      "maximum": 90
                    },
                    "lng": {
                      "type": "number",
                      "minimum": -180,
                      "maximum": 180
                    },
                    "source": {
                      "type": "string",
                      "enum": [
                        "typed",
                        "maps-link",
                        "pin",
                        "coordinates"
                      ]
                    }
                  },
                  "required": [
                    "name",
                    "lat",
                    "lng"
                  ],
                  "additionalProperties": false
                }
              },
              "source": {
                "type": "string",
                "enum": [
                  "typed",
                  "maps-link",
                  "pin",
                  "coordinates"
                ]
              }
            },
            "required": [
              "location",
              "lat",
              "lng"
            ],
            "additionalProperties": false
          }
        },
        "body": {
          "type": "string"
        },
        "mode": {
          "type": "string",
          "enum": [
            "nights",
            "dates"
          ]
        },
        "readers": {
          "type": "string",
          "enum": [
            "map",
            "details"
          ]
        },
        "private": {
          "type": "object",
          "properties": {
            "links": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "label": {
                    "type": "string",
                    "minLength": 1
                  },
                  "url": {
                    "type": "string",
                    "format": "uri"
                  }
                },
                "required": [
                  "label",
                  "url"
                ],
                "additionalProperties": false
              }
            },
            "stops": {
              "type": "object",
              "propertyNames": {
                "type": "string"
              },
              "additionalProperties": {
                "type": "object",
                "properties": {
                  "stay": {
                    "type": "object",
                    "properties": {
                      "name": {
                        "type": "string",
                        "minLength": 1
                      },
                      "lat": {
                        "type": "number",
                        "minimum": -90,
                        "maximum": 90
                      },
                      "lng": {
                        "type": "number",
                        "minimum": -180,
                        "maximum": 180
                      }
                    },
                    "required": [
                      "name",
                      "lat",
                      "lng"
                    ],
                    "additionalProperties": false
                  },
                  "links": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "label": {
                          "type": "string",
                          "minLength": 1
                        },
                        "url": {
                          "type": "string",
                          "format": "uri"
                        }
                      },
                      "required": [
                        "label",
                        "url"
                      ],
                      "additionalProperties": false
                    }
                  }
                },
                "additionalProperties": false
              }
            }
          },
          "additionalProperties": false
        }
      },
      "required": [
        "route"
      ],
      "additionalProperties": false
    },
    "days": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}-[a-z0-9]+(-[a-z0-9]+)*$"
          },
          "title": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "date": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
          },
          "content": {
            "type": "string",
            "maxLength": 100000
          },
          "media": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "src": {
                  "type": "string"
                },
                "caption": {
                  "type": "string"
                },
                "visibility": {
                  "type": "string",
                  "enum": [
                    "guest",
                    "private"
                  ]
                }
              },
              "required": [
                "src"
              ],
              "additionalProperties": false
            }
          },
          "costs": {
            "maxItems": 200,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "label": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 200
                },
                "amount": {
                  "type": "number",
                  "exclusiveMinimum": 0
                },
                "category": {
                  "type": "string",
                  "enum": [
                    "preparation",
                    "flights",
                    "accommodation",
                    "food",
                    "transport",
                    "activities",
                    "other"
                  ]
                },
                "currency": {
                  "type": "string",
                  "minLength": 3,
                  "maxLength": 3
                },
                "stop": {
                  "type": "string"
                }
              },
              "required": [
                "label",
                "amount"
              ],
              "additionalProperties": false
            }
          },
          "coordinates": {
            "type": "object",
            "properties": {
              "lat": {
                "type": "number",
                "minimum": -90,
                "maximum": 90
              },
              "lng": {
                "type": "number",
                "minimum": -180,
                "maximum": 180
              }
            },
            "required": [
              "lat",
              "lng"
            ],
            "additionalProperties": false
          },
          "weather": {
            "anyOf": [
              {
                "type": "boolean",
                "const": true
              },
              {
                "type": "object",
                "properties": {
                  "tempMin": {
                    "type": "number",
                    "minimum": -90,
                    "maximum": 60
                  },
                  "tempMax": {
                    "type": "number",
                    "minimum": -90,
                    "maximum": 60
                  },
                  "code": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 99
                  },
                  "precipitation": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 2000
                  },
                  "windMax": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 500
                  },
                  "recordedAt": {
                    "type": "string",
                    "format": "date-time",
                    "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
                  },
                  "source": {
                    "type": "string",
                    "minLength": 1
                  }
                },
                "required": [
                  "recordedAt",
                  "source"
                ],
                "additionalProperties": false
              }
            ]
          },
          "time": {
            "type": "string",
            "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$"
          },
          "timezone": {
            "type": "string"
          },
          "location": {
            "type": "string"
          },
          "country": {
            "type": "string"
          },
          "countryCode": {
            "type": "string",
            "minLength": 2,
            "maxLength": 2
          },
          "transportMode": {
            "type": "string",
            "enum": [
              "flight",
              "train",
              "bus",
              "motorbike",
              "bicycle",
              "boat",
              "car",
              "taxi",
              "walk",
              "metro",
              "tram",
              "ferry"
            ]
          },
          "tags": {
            "maxItems": 10,
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 30,
              "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$"
            }
          },
          "translations": {
            "type": "object",
            "propertyNames": {
              "type": "string"
            },
            "additionalProperties": {
              "type": "object",
              "properties": {
                "title": {
                  "type": "string"
                },
                "content": {
                  "type": "string"
                }
              },
              "required": [
                "title",
                "content"
              ],
              "additionalProperties": false
            }
          },
          "visibility": {
            "type": "string",
            "enum": [
              "guest",
              "private"
            ]
          },
          "status": {
            "type": "string",
            "const": "draft"
          },
          "declined": {
            "type": "object",
            "propertyNames": {
              "type": "string",
              "enum": [
                "media",
                "costs",
                "coordinates",
                "weather",
                "time",
                "timezone",
                "location",
                "country",
                "countryCode",
                "transportMode",
                "tags",
                "translations",
                "visibility",
                "status"
              ]
            },
            "additionalProperties": {
              "type": "string",
              "minLength": 10
            }
          },
          "transportFrom": {
            "type": "string"
          },
          "transportTo": {
            "type": "string"
          },
          "travelScene": {
            "type": "string",
            "enum": [
              "default",
              "quick",
              "skip"
            ]
          },
          "test": {
            "type": "boolean"
          }
        },
        "required": [
          "slug",
          "title",
          "date",
          "content"
        ],
        "additionalProperties": false
      }
    },
    "translations": {
      "type": "object",
      "propertyNames": {
        "type": "string"
      },
      "additionalProperties": {
        "type": "object",
        "properties": {
          "title": {
            "type": "string"
          },
          "tagline": {
            "type": "string"
          },
          "intro": {
            "type": "string"
          }
        },
        "additionalProperties": false
      }
    },
    "accent": {
      "type": "string",
      "enum": [
        "sky",
        "yellow",
        "green",
        "coral",
        "navy"
      ]
    },
    "cover": {
      "type": "string"
    },
    "figures": {
      "anyOf": [
        {
          "type": "object",
          "properties": {
            "mode": {
              "type": "string",
              "const": "off"
            }
          },
          "required": [
            "mode"
          ],
          "additionalProperties": false
        },
        {
          "type": "object",
          "properties": {
            "mode": {
              "type": "string",
              "const": "journal"
            }
          },
          "required": [
            "mode"
          ],
          "additionalProperties": false
        },
        {
          "type": "object",
          "properties": {
            "mode": {
              "type": "string",
              "const": "custom"
            },
            "figures": {
              "minItems": 1,
              "maxItems": 10,
              "type": "array",
              "items": {
                "type": "string",
                "minLength": 1
              }
            }
          },
          "required": [
            "mode",
            "figures"
          ],
          "additionalProperties": false
        }
      ]
    },
    "tagline": {
      "type": "string"
    },
    "intro": {
      "type": "string"
    },
    "listed": {
      "type": "boolean"
    },
    "reminder": {
      "type": "object",
      "properties": {
        "channel": {
          "type": "string",
          "enum": [
            "mail"
          ]
        }
      },
      "required": [
        "channel"
      ],
      "additionalProperties": false
    },
    "declined": {
      "type": "object",
      "propertyNames": {
        "type": "string",
        "enum": [
          "rates",
          "costs",
          "plan",
          "days",
          "translations",
          "accent",
          "cover",
          "figures",
          "tagline",
          "intro",
          "listed"
        ]
      },
      "additionalProperties": {
        "type": "string",
        "minLength": 10
      }
    },
    "test": {
      "type": "boolean"
    }
  },
  "required": [
    "id",
    "title",
    "dates",
    "visibility",
    "people"
  ],
  "additionalProperties": false,
  "x-required-or-declined": [
    {
      "field": "rates",
      "whyRequired": "every trip states the currencies its figures may use, or declines them",
      "toDecline": "declined.rates"
    },
    {
      "field": "costs",
      "whyRequired": "every trip carries a budget, or says why costs are not tracked here",
      "toDecline": "declined.costs"
    },
    {
      "field": "plan",
      "whyRequired": "an upcoming trip carries its intended route, or says why there is none",
      "toDecline": "declined.plan"
    },
    {
      "field": "days",
      "whyRequired": "a trip carries its days, or says why there are none yet (e.g. it has not started)",
      "toDecline": "declined.days"
    },
    {
      "field": "translations",
      "whyRequired": "a journal that maintains several languages carries each trip's title and tagline in all of them, or says why not (a single-language journal is exempt — the route skips this check)",
      "toDecline": "declined.translations"
    },
    {
      "field": "accent",
      "whyRequired": "every trip picks the colour its cards are drawn in, or leaves it to the default with a reason",
      "toDecline": "declined.accent"
    },
    {
      "field": "figures",
      "whyRequired": "how the party is drawn as walking figures: {mode: \"off\"}, {mode: \"journal\"} (the journal's default set), or {mode: \"custom\", figures: [ids]} — create figures at /figures first",
      "toDecline": "declined.figures"
    },
    {
      "field": "tagline",
      "whyRequired": "every trip card carries its one-line subtitle, or a reason it has none",
      "toDecline": "declined.tagline"
    },
    {
      "field": "intro",
      "whyRequired": "a trip page opens with a few lines of prose, or says why there are none",
      "toDecline": "declined.intro"
    }
  ],
  "allOf": [
    {
      "anyOf": [
        {
          "required": [
            "rates"
          ]
        },
        {
          "required": [
            "declined"
          ],
          "properties": {
            "declined": {
              "required": [
                "rates"
              ]
            }
          }
        }
      ]
    },
    {
      "anyOf": [
        {
          "required": [
            "costs"
          ]
        },
        {
          "required": [
            "declined"
          ],
          "properties": {
            "declined": {
              "required": [
                "costs"
              ]
            }
          }
        }
      ]
    },
    {
      "anyOf": [
        {
          "required": [
            "plan"
          ]
        },
        {
          "required": [
            "declined"
          ],
          "properties": {
            "declined": {
              "required": [
                "plan"
              ]
            }
          }
        }
      ]
    },
    {
      "anyOf": [
        {
          "required": [
            "days"
          ]
        },
        {
          "required": [
            "declined"
          ],
          "properties": {
            "declined": {
              "required": [
                "days"
              ]
            }
          }
        }
      ]
    },
    {
      "anyOf": [
        {
          "required": [
            "translations"
          ]
        },
        {
          "required": [
            "declined"
          ],
          "properties": {
            "declined": {
              "required": [
                "translations"
              ]
            }
          }
        }
      ]
    },
    {
      "anyOf": [
        {
          "required": [
            "accent"
          ]
        },
        {
          "required": [
            "declined"
          ],
          "properties": {
            "declined": {
              "required": [
                "accent"
              ]
            }
          }
        }
      ]
    },
    {
      "anyOf": [
        {
          "required": [
            "figures"
          ]
        },
        {
          "required": [
            "declined"
          ],
          "properties": {
            "declined": {
              "required": [
                "figures"
              ]
            }
          }
        }
      ]
    },
    {
      "anyOf": [
        {
          "required": [
            "tagline"
          ]
        },
        {
          "required": [
            "declined"
          ],
          "properties": {
            "declined": {
              "required": [
                "tagline"
              ]
            }
          }
        }
      ]
    },
    {
      "anyOf": [
        {
          "required": [
            "intro"
          ]
        },
        {
          "required": [
            "declined"
          ],
          "properties": {
            "declined": {
              "required": [
                "intro"
              ]
            }
          }
        }
      ]
    }
  ]
}

Responses

200

replaced (If-Match matched the stored ETag)

201

created — carries next when this is the journal's first trip

400

invalid_trip_id; invalid_request; invalid_trip; invalid_translations; invalid_cover

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

no_such_journal

409

stale_document — exists already, with no or a stale If-Match — carries the stored document

413

body_too_large — the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status

422

incomplete

/api/v2/{user}/trips/{trip}/rename

postRename a trip's id — the folder, trip.json's own id, every database row a trip_id column names, and a redirect record so the old address keeps answering. Never touches gps/.

Request body

application/json

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "id": {
      "type": "string",
      "pattern": "^[a-z0-9][a-z0-9-]*$"
    }
  },
  "required": [
    "id"
  ],
  "additionalProperties": false
}

Responses

200

renamed — GET the trip at its new id from here on

400

invalid_trip_id — not a valid slug, or the same id the trip already has; invalid_request — the new id is the trip's current one — nothing to rename

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

no_such_journal; unknown_trip

409

trip_id_taken

413

body_too_large — the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status

/api/v2/{user}/trips/{trip}/days

getEvery day in this trip, paged.

Responses

200

one page of day documents

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

unknown_trip

/api/v2/{user}/trips/{trip}/days/{slug}

getOne day.

Responses

200

the day, with its ETag

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

unknown_trip; unknown_day

patchMerge-patch a day. Never moves it between draft and published. A draft may stay incomplete — completeness is asked at publish; a published day must stay complete (422 incomplete). A day that changed while the patch was checked (a publish, another write) answers 409 stale_document.

Request body

application/json

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "slug": {
      "type": "string",
      "pattern": "^\\d{4}-\\d{2}-\\d{2}-[a-z0-9]+(-[a-z0-9]+)*$"
    },
    "title": {
      "type": "string",
      "minLength": 1,
      "maxLength": 200
    },
    "date": {
      "type": "string",
      "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
    },
    "content": {
      "type": "string",
      "maxLength": 100000
    },
    "media": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "src": {
            "type": "string"
          },
          "caption": {
            "type": "string"
          },
          "visibility": {
            "type": "string",
            "enum": [
              "guest",
              "private"
            ]
          }
        },
        "required": [
          "src"
        ],
        "additionalProperties": false
      }
    },
    "costs": {
      "maxItems": 200,
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "label": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "amount": {
            "type": "number",
            "exclusiveMinimum": 0
          },
          "category": {
            "type": "string",
            "enum": [
              "preparation",
              "flights",
              "accommodation",
              "food",
              "transport",
              "activities",
              "other"
            ]
          },
          "currency": {
            "type": "string",
            "minLength": 3,
            "maxLength": 3
          },
          "stop": {
            "type": "string"
          }
        },
        "required": [
          "label",
          "amount"
        ],
        "additionalProperties": false
      }
    },
    "coordinates": {
      "type": "object",
      "properties": {
        "lat": {
          "type": "number",
          "minimum": -90,
          "maximum": 90
        },
        "lng": {
          "type": "number",
          "minimum": -180,
          "maximum": 180
        }
      },
      "required": [
        "lat",
        "lng"
      ],
      "additionalProperties": false
    },
    "weather": {
      "anyOf": [
        {
          "type": "boolean",
          "const": true
        },
        {
          "type": "object",
          "properties": {
            "tempMin": {
              "type": "number",
              "minimum": -90,
              "maximum": 60
            },
            "tempMax": {
              "type": "number",
              "minimum": -90,
              "maximum": 60
            },
            "code": {
              "type": "integer",
              "minimum": 0,
              "maximum": 99
            },
            "precipitation": {
              "type": "number",
              "minimum": 0,
              "maximum": 2000
            },
            "windMax": {
              "type": "number",
              "minimum": 0,
              "maximum": 500
            },
            "recordedAt": {
              "type": "string",
              "format": "date-time",
              "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
            },
            "source": {
              "type": "string",
              "minLength": 1
            }
          },
          "required": [
            "recordedAt",
            "source"
          ],
          "additionalProperties": false
        }
      ]
    },
    "time": {
      "type": "string",
      "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$"
    },
    "timezone": {
      "type": "string"
    },
    "location": {
      "type": "string"
    },
    "country": {
      "type": "string"
    },
    "countryCode": {
      "type": "string",
      "minLength": 2,
      "maxLength": 2
    },
    "transportMode": {
      "type": "string",
      "enum": [
        "flight",
        "train",
        "bus",
        "motorbike",
        "bicycle",
        "boat",
        "car",
        "taxi",
        "walk",
        "metro",
        "tram",
        "ferry"
      ]
    },
    "tags": {
      "maxItems": 10,
      "type": "array",
      "items": {
        "type": "string",
        "maxLength": 30,
        "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$"
      }
    },
    "translations": {
      "type": "object",
      "propertyNames": {
        "type": "string"
      },
      "additionalProperties": {
        "type": "object",
        "properties": {
          "title": {
            "type": "string"
          },
          "content": {
            "type": "string"
          }
        },
        "required": [
          "title",
          "content"
        ],
        "additionalProperties": false
      }
    },
    "visibility": {
      "type": "string",
      "enum": [
        "guest",
        "private"
      ]
    },
    "status": {
      "type": "string",
      "const": "draft"
    },
    "declined": {
      "type": "object",
      "propertyNames": {
        "type": "string",
        "enum": [
          "media",
          "costs",
          "coordinates",
          "weather",
          "time",
          "timezone",
          "location",
          "country",
          "countryCode",
          "transportMode",
          "tags",
          "translations",
          "visibility",
          "status"
        ]
      },
      "additionalProperties": {
        "type": "string",
        "minLength": 10
      }
    },
    "transportFrom": {
      "type": "string"
    },
    "transportTo": {
      "type": "string"
    },
    "travelScene": {
      "type": "string",
      "enum": [
        "default",
        "quick",
        "skip"
      ]
    },
    "test": {
      "type": "boolean"
    }
  },
  "additionalProperties": false
}

Responses

200

the merged, re-validated document

400

invalid_request; weather_disabled; invalid_entry; invalid_translations

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

unknown_trip; unknown_day

409

stale_document

413

body_too_large — the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status

422

incomplete

deleteDelete a draft day outright — no confirmation step, since it was never on the site.

Responses

200

deleted; its media is kept on disk

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

unknown_trip; unknown_day

409

published_day_not_deletable

putCreate a day at a client-chosen slug, or replace a draft (with a matching If-Match).

Request body

application/json

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "slug": {
      "type": "string",
      "pattern": "^\\d{4}-\\d{2}-\\d{2}-[a-z0-9]+(-[a-z0-9]+)*$"
    },
    "title": {
      "type": "string",
      "minLength": 1,
      "maxLength": 200
    },
    "date": {
      "type": "string",
      "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
    },
    "content": {
      "type": "string",
      "maxLength": 100000
    },
    "media": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "src": {
            "type": "string"
          },
          "caption": {
            "type": "string"
          },
          "visibility": {
            "type": "string",
            "enum": [
              "guest",
              "private"
            ]
          }
        },
        "required": [
          "src"
        ],
        "additionalProperties": false
      }
    },
    "costs": {
      "maxItems": 200,
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "label": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "amount": {
            "type": "number",
            "exclusiveMinimum": 0
          },
          "category": {
            "type": "string",
            "enum": [
              "preparation",
              "flights",
              "accommodation",
              "food",
              "transport",
              "activities",
              "other"
            ]
          },
          "currency": {
            "type": "string",
            "minLength": 3,
            "maxLength": 3
          },
          "stop": {
            "type": "string"
          }
        },
        "required": [
          "label",
          "amount"
        ],
        "additionalProperties": false
      }
    },
    "coordinates": {
      "type": "object",
      "properties": {
        "lat": {
          "type": "number",
          "minimum": -90,
          "maximum": 90
        },
        "lng": {
          "type": "number",
          "minimum": -180,
          "maximum": 180
        }
      },
      "required": [
        "lat",
        "lng"
      ],
      "additionalProperties": false
    },
    "weather": {
      "anyOf": [
        {
          "type": "boolean",
          "const": true
        },
        {
          "type": "object",
          "properties": {
            "tempMin": {
              "type": "number",
              "minimum": -90,
              "maximum": 60
            },
            "tempMax": {
              "type": "number",
              "minimum": -90,
              "maximum": 60
            },
            "code": {
              "type": "integer",
              "minimum": 0,
              "maximum": 99
            },
            "precipitation": {
              "type": "number",
              "minimum": 0,
              "maximum": 2000
            },
            "windMax": {
              "type": "number",
              "minimum": 0,
              "maximum": 500
            },
            "recordedAt": {
              "type": "string",
              "format": "date-time",
              "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
            },
            "source": {
              "type": "string",
              "minLength": 1
            }
          },
          "required": [
            "recordedAt",
            "source"
          ],
          "additionalProperties": false
        }
      ]
    },
    "time": {
      "type": "string",
      "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$"
    },
    "timezone": {
      "type": "string"
    },
    "location": {
      "type": "string"
    },
    "country": {
      "type": "string"
    },
    "countryCode": {
      "type": "string",
      "minLength": 2,
      "maxLength": 2
    },
    "transportMode": {
      "type": "string",
      "enum": [
        "flight",
        "train",
        "bus",
        "motorbike",
        "bicycle",
        "boat",
        "car",
        "taxi",
        "walk",
        "metro",
        "tram",
        "ferry"
      ]
    },
    "tags": {
      "maxItems": 10,
      "type": "array",
      "items": {
        "type": "string",
        "maxLength": 30,
        "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$"
      }
    },
    "translations": {
      "type": "object",
      "propertyNames": {
        "type": "string"
      },
      "additionalProperties": {
        "type": "object",
        "properties": {
          "title": {
            "type": "string"
          },
          "content": {
            "type": "string"
          }
        },
        "required": [
          "title",
          "content"
        ],
        "additionalProperties": false
      }
    },
    "visibility": {
      "type": "string",
      "enum": [
        "guest",
        "private"
      ]
    },
    "status": {
      "type": "string",
      "const": "draft"
    },
    "declined": {
      "type": "object",
      "propertyNames": {
        "type": "string",
        "enum": [
          "media",
          "costs",
          "coordinates",
          "weather",
          "time",
          "timezone",
          "location",
          "country",
          "countryCode",
          "transportMode",
          "tags",
          "translations",
          "visibility",
          "status"
        ]
      },
      "additionalProperties": {
        "type": "string",
        "minLength": 10
      }
    },
    "transportFrom": {
      "type": "string"
    },
    "transportTo": {
      "type": "string"
    },
    "travelScene": {
      "type": "string",
      "enum": [
        "default",
        "quick",
        "skip"
      ]
    },
    "test": {
      "type": "boolean"
    }
  },
  "required": [
    "slug",
    "title",
    "date",
    "content"
  ],
  "additionalProperties": false,
  "x-required-or-declined": [
    {
      "field": "media",
      "whyRequired": "a day names the photographs on it (by the src the media door answered with), or says why there are none",
      "toDecline": "declined.media"
    },
    {
      "field": "costs",
      "whyRequired": "a day carries what was spent on it, or says why not (nothing spent / figures lost / owner tracks costs elsewhere)",
      "toDecline": "declined.costs"
    },
    {
      "field": "coordinates",
      "whyRequired": "a day carries where it happened (lat/lng), or says why there is no position",
      "toDecline": "declined.coordinates"
    },
    {
      "field": "weather",
      "whyRequired": "a day asks the server to look its weather up (weather: true), brings a real reading, or declines",
      "toDecline": "declined.weather"
    },
    {
      "field": "time",
      "whyRequired": "a day says when it happened (HH:MM, local), or declines",
      "toDecline": "declined.time"
    },
    {
      "field": "timezone",
      "whyRequired": "the IANA zone the time is local to, or a reason it is not known",
      "toDecline": "declined.timezone"
    },
    {
      "field": "location",
      "whyRequired": "the place this day happened, or why none is named",
      "toDecline": "declined.location"
    },
    {
      "field": "country",
      "whyRequired": "the country this day happened in, or why none is named",
      "toDecline": "declined.country"
    },
    {
      "field": "countryCode",
      "whyRequired": "the ISO-2 code behind the flag the day card draws, or a decline",
      "toDecline": "declined.countryCode"
    },
    {
      "field": "transportMode",
      "whyRequired": "how this day travelled, or a decline for a day with no leg (a rest day)",
      "toDecline": "declined.transportMode"
    },
    {
      "field": "tags",
      "whyRequired": "the day's tags, or a decline",
      "toDecline": "declined.tags"
    },
    {
      "field": "translations",
      "whyRequired": "a journal that maintains several languages carries the day in all of them, or declines — a declined translation falls back to the main language (a single-language journal is exempt; the route skips this check)",
      "toDecline": "declined.translations"
    },
    {
      "field": "visibility",
      "whyRequired": "whether this day is held back (guest or private, narrowing the trip's own gate), or declined — declined means shown to everyone the trip lets in",
      "toDecline": "declined.visibility"
    },
    {
      "field": "status",
      "whyRequired": "a day states it arrives as a draft — status: \"draft\" is the only writable value; publishing stays its own call",
      "toDecline": "declined.status"
    }
  ],
  "allOf": [
    {
      "anyOf": [
        {
          "required": [
            "media"
          ]
        },
        {
          "required": [
            "declined"
          ],
          "properties": {
            "declined": {
              "required": [
                "media"
              ]
            }
          }
        }
      ]
    },
    {
      "anyOf": [
        {
          "required": [
            "costs"
          ]
        },
        {
          "required": [
            "declined"
          ],
          "properties": {
            "declined": {
              "required": [
                "costs"
              ]
            }
          }
        }
      ]
    },
    {
      "anyOf": [
        {
          "required": [
            "coordinates"
          ]
        },
        {
          "required": [
            "declined"
          ],
          "properties": {
            "declined": {
              "required": [
                "coordinates"
              ]
            }
          }
        }
      ]
    },
    {
      "anyOf": [
        {
          "required": [
            "weather"
          ]
        },
        {
          "required": [
            "declined"
          ],
          "properties": {
            "declined": {
              "required": [
                "weather"
              ]
            }
          }
        }
      ]
    },
    {
      "anyOf": [
        {
          "required": [
            "time"
          ]
        },
        {
          "required": [
            "declined"
          ],
          "properties": {
            "declined": {
              "required": [
                "time"
              ]
            }
          }
        }
      ]
    },
    {
      "anyOf": [
        {
          "required": [
            "timezone"
          ]
        },
        {
          "required": [
            "declined"
          ],
          "properties": {
            "declined": {
              "required": [
                "timezone"
              ]
            }
          }
        }
      ]
    },
    {
      "anyOf": [
        {
          "required": [
            "location"
          ]
        },
        {
          "required": [
            "declined"
          ],
          "properties": {
            "declined": {
              "required": [
                "location"
              ]
            }
          }
        }
      ]
    },
    {
      "anyOf": [
        {
          "required": [
            "country"
          ]
        },
        {
          "required": [
            "declined"
          ],
          "properties": {
            "declined": {
              "required": [
                "country"
              ]
            }
          }
        }
      ]
    },
    {
      "anyOf": [
        {
          "required": [
            "countryCode"
          ]
        },
        {
          "required": [
            "declined"
          ],
          "properties": {
            "declined": {
              "required": [
                "countryCode"
              ]
            }
          }
        }
      ]
    },
    {
      "anyOf": [
        {
          "required": [
            "transportMode"
          ]
        },
        {
          "required": [
            "declined"
          ],
          "properties": {
            "declined": {
              "required": [
                "transportMode"
              ]
            }
          }
        }
      ]
    },
    {
      "anyOf": [
        {
          "required": [
            "tags"
          ]
        },
        {
          "required": [
            "declined"
          ],
          "properties": {
            "declined": {
              "required": [
                "tags"
              ]
            }
          }
        }
      ]
    },
    {
      "anyOf": [
        {
          "required": [
            "translations"
          ]
        },
        {
          "required": [
            "declined"
          ],
          "properties": {
            "declined": {
              "required": [
                "translations"
              ]
            }
          }
        }
      ]
    },
    {
      "anyOf": [
        {
          "required": [
            "visibility"
          ]
        },
        {
          "required": [
            "declined"
          ],
          "properties": {
            "declined": {
              "required": [
                "visibility"
              ]
            }
          }
        }
      ]
    },
    {
      "anyOf": [
        {
          "required": [
            "status"
          ]
        },
        {
          "required": [
            "declined"
          ],
          "properties": {
            "declined": {
              "required": [
                "status"
              ]
            }
          }
        }
      ]
    }
  ]
}

Responses

200

replaced

201

created — carries next when this is the trip's first day

400

invalid_request; weather_disabled; invalid_entry; invalid_translations

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

unknown_trip

409

stale_document; already_published — PUT replaces a draft only; PATCH a published day

413

body_too_large — the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status

422

incomplete

/api/v2/{user}/trips/{trip}/days/{slug}/media

postAttach already-stored photographs to this day's gallery — never uploads bytes itself (POST /api/v2/{user}/media does that first). Retracts a stale declined.media.

Request body

application/json

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "items": {
      "minItems": 1,
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "src": {
            "type": "string",
            "minLength": 1
          },
          "caption": {
            "type": "string"
          },
          "visibility": {
            "type": "string",
            "enum": [
              "guest",
              "private"
            ]
          }
        },
        "required": [
          "src"
        ],
        "additionalProperties": false
      }
    }
  },
  "required": [
    "items"
  ],
  "additionalProperties": false
}

Responses

200

the day, with the photographs attached

400

invalid_request; not_this_trip

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

unknown_trip; unknown_day

409

stale_document

413

body_too_large — the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status

deleteTake photographs off this day's gallery by src — the reversible half; the bytes stay on disk (DELETE /api/v2/{user}/media removes those, and detaches from every day too).

Request body

application/json

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "srcs": {
      "minItems": 1,
      "type": "array",
      "items": {
        "type": "string",
        "minLength": 1
      }
    }
  },
  "required": [
    "srcs"
  ],
  "additionalProperties": false
}

Responses

200

the day, with the photographs detached

400

invalid_request

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

unknown_trip; unknown_day; unknown_media

409

stale_document

413

body_too_large — the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status

/api/v2/{user}/trips/{trip}/days/{slug}/publish

postOwner only: put a draft day on the site. declineTracked may name only fields the day has neither filled in nor already answered; any other name refuses the whole call (400, details.refused).

Request body

application/json

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "declineTracked": {
      "type": "array",
      "items": {
        "type": "string",
        "enum": [
          "media",
          "costs",
          "coordinates",
          "weather",
          "time",
          "timezone",
          "location",
          "country",
          "countryCode",
          "transportMode",
          "tags",
          "translations",
          "visibility",
          "status"
        ]
      }
    },
    "sendMail": {
      "type": "boolean"
    },
    "sendWhatsapp": {
      "type": "boolean"
    }
  },
  "additionalProperties": false
}

Responses

200

dryRun — nothing written

400

invalid_request — includes declineTracked naming a field that is not blank (details.refused)

401

missing_token; invalid_token

402

no_credits

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it; out_of_scope — a trip-scoped token cannot publish

404

unknown_trip; unknown_day

409

already_published; stale_document — the day changed while the publish was checked; nothing written

413

body_too_large — the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status

422

incomplete_day

/api/v2/{user}/trips/{trip}/days/{slug}/unpublish

postOwner only: take a published day off the site. Reversible by publishing again.

Responses

200

off the site

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it; out_of_scope — a trip-scoped token cannot unpublish

404

unknown_trip; unknown_day

409

already_draft

/api/v2/{user}/trips/{trip}/days/{slug}/send

postOwner only: send (or resend) a published day by mail and/or WhatsApp. The one send door.

Request body

application/json

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "channels": {
      "minItems": 1,
      "type": "array",
      "items": {
        "type": "string",
        "enum": [
          "mail",
          "whatsapp"
        ]
      }
    }
  },
  "required": [
    "channels"
  ],
  "additionalProperties": false
}

Responses

200

per-channel send summaries

400

invalid_request

401

missing_token; invalid_token

402

no_credits

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it; out_of_scope — a trip-scoped token cannot send

404

unknown_trip; unknown_day

409

not_published; test_content

413

body_too_large — the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status

/api/v2/{user}/trips/{trip}/days/{slug}/move

postMove a day to a different date and/or a different trip, keeping its media the same print masters.

Request body

application/json

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "toTripId": {
      "type": "string",
      "pattern": "^[a-z0-9][a-z0-9-]*$"
    },
    "date": {
      "type": "string",
      "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
    }
  },
  "required": [
    "toTripId",
    "date"
  ],
  "additionalProperties": false
}

Responses

200

moved — GET the returned tripId/slug to see it

400

invalid_date; invalid_request

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

no_such_journal; unknown_trip; unknown_day

409

already_exists

413

body_too_large — the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status

/api/v2/{user}/trips/{trip}/days/{slug}/split

postSplit one day's update into two, on the same date. The prose is never cut automatically.

Request body

application/json

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "photoCutIndex": {
      "type": "integer",
      "minimum": 0,
      "maximum": 9007199254740991
    },
    "firstContent": {
      "type": "string"
    },
    "secondTitle": {
      "type": "string"
    },
    "secondContent": {
      "type": "string"
    },
    "secondTime": {
      "type": "string",
      "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$"
    }
  },
  "required": [
    "photoCutIndex",
    "firstContent",
    "secondTitle",
    "secondContent"
  ],
  "additionalProperties": false
}

Responses

200

split — GET secondSlug to see the new half, always a draft

400

invalid_request

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

no_such_journal; unknown_trip; unknown_day

409

title_required; slug_taken

413

body_too_large — the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status

/api/v2/{user}/trips/{trip}/days/{slug}/merge

postJoin two updates on the same trip into one. Refused across trips.

Request body

application/json

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "withTripId": {
      "type": "string",
      "pattern": "^[a-z0-9][a-z0-9-]*$"
    },
    "withSlug": {
      "type": "string",
      "pattern": "^\\d{4}-\\d{2}-\\d{2}-[a-z0-9]+(-[a-z0-9]+)*$"
    }
  },
  "required": [
    "withSlug"
  ],
  "additionalProperties": false
}

Responses

200

merged — GET the returned slug to see it

400

invalid_request

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

no_such_journal; unknown_trip; unknown_day

409

cross_trip

413

body_too_large — the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status

/api/v2/{user}/trips/{trip}/costs/apply

postWrite agreed bank-statement rows onto the days they belong to — a row whose date has no day yet is filed to the trip's own costs.items instead, never dropped.

Request body

application/json

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "rows": {
      "minItems": 1,
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "date": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
          },
          "label": {
            "type": "string",
            "minLength": 1
          },
          "amount": {
            "type": "number",
            "exclusiveMinimum": 0
          },
          "currency": {
            "type": "string",
            "minLength": 3,
            "maxLength": 3
          },
          "category": {
            "type": "string",
            "enum": [
              "preparation",
              "flights",
              "accommodation",
              "food",
              "transport",
              "activities",
              "other"
            ]
          }
        },
        "required": [
          "date",
          "label",
          "amount",
          "currency",
          "category"
        ],
        "additionalProperties": false
      }
    },
    "statement": {
      "type": "string"
    }
  },
  "required": [
    "rows"
  ],
  "additionalProperties": false
}

Responses

200

how many rows landed on a day, and which dates were filed to the trip instead

400

invalid_costs

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

unknown_trip

413

body_too_large — the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status

/api/v2/{user}/media

getOne trip's stored media, paged — send ?trip=.

Responses

200

one page of media items

400

invalid_request

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

unknown_trip

postUpload bytes: JSON `{intent, url|inbox}` fetching/resolving the bytes, or `multipart/form-data` with `file` and `intent`. `intent.kind` is one of photo, bank_export, gps_history, document.

Request body

application/json

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "kind": {
      "type": "string",
      "enum": [
        "photo",
        "bank_export",
        "gps_history",
        "document"
      ]
    },
    "trip": {
      "type": "string",
      "minLength": 1
    },
    "day": {
      "type": "string",
      "minLength": 1
    },
    "caption": {
      "type": "string",
      "minLength": 1
    },
    "format": {
      "type": "string",
      "minLength": 1
    },
    "declined": {
      "type": "object",
      "propertyNames": {
        "type": "string",
        "enum": [
          "trip",
          "day",
          "caption",
          "format"
        ]
      },
      "additionalProperties": {
        "type": "string",
        "minLength": 10
      }
    }
  },
  "required": [
    "kind"
  ],
  "additionalProperties": false
}

Responses

201

stored

400

invalid_request; expected_urls; unknown_inbox_file; could_not_fetch; expected_multipart; storage_full; invalid_media

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

unknown_trip; unknown_day

413

body_too_large

deleteRemove a stored media item by its src.

Responses

200

removed

400

invalid_request

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

unknown_media

/api/v2/{user}/trips/{trip}/media/duplicates

getThe same photograph, twice — reports only, never deletes.

Responses

200

groups of look-alike photographs, largest first

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

unknown_trip

/api/v2/{user}/gps/zones

getThe journal's private zones — a home, a place clipped out of every future track.

Responses

200

the zones, the decline flag, the limits, and an ETag — send it back as If-Match on PUT

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

no_such_journal

500

unreadable_zones

putReplace the zone list. Requires If-Match with the ETag GET last answered — refused without one, or with a stale one, as stale_document (409). Label a zone exactly "home" to arm the recorder's own gate (B2196/B2198).

Request body

application/json

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "zones": {
      "maxItems": 20,
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "label": {
            "type": "string",
            "minLength": 1,
            "maxLength": 80
          },
          "lat": {
            "type": "number",
            "minimum": -90,
            "maximum": 90
          },
          "lon": {
            "type": "number",
            "minimum": -180,
            "maximum": 180
          },
          "radiusM": {
            "type": "number",
            "minimum": 50,
            "maximum": 5000
          }
        },
        "required": [
          "label",
          "lat",
          "lon",
          "radiusM"
        ],
        "additionalProperties": false
      }
    },
    "homeDeclined": {
      "type": "boolean"
    }
  },
  "required": [
    "zones"
  ],
  "additionalProperties": false
}

Responses

200

the zones, the decline flag, and the limits, as stored, with the new ETag

400

invalid_request

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

no_such_journal

409

stale_document — no If-Match, or one that does not cover the current ETag — carries the stored document

413

body_too_large — the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status

500

unreadable_zones

/api/v2/{user}/inbox

getFiles staged with no trip/day yet, plus every photograph filed onto a trip with a declined day.

Responses

200

counts and items with no trip yet, by shelf; tripWaiting for the trip-scoped, still day-less kind

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

no_such_journal

/api/v2/{user}/inbox/{id}

deleteDiscard one staged file. No confirmation — nothing here has ever been on the site.

Responses

200

removed

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

no_such_journal; not_found

/api/v2/{user}/figures

getEvery figure in the journal's library, paged.

Responses

200

one page of figures

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

no_such_journal

/api/v2/{user}/figures/{id}

getOne figure.

Responses

200

the figure

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

no_such_journal; not_found

deleteDelete a figure — refused while any trip or the journal's own default set still names it.

Responses

200

deleted

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

no_such_journal; not_found

409

figure_referenced

putCreate a figure at a client-chosen id, or replace one (with a matching If-Match).

Request body

application/json

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "id": {
      "type": "string",
      "pattern": "^[a-z0-9][a-z0-9-]*$"
    },
    "name": {
      "type": "string",
      "minLength": 1
    },
    "person": {
      "type": "string",
      "format": "email",
      "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
    },
    "hairStyle": {
      "type": "string",
      "enum": [
        "buzz",
        "short",
        "tousled",
        "long",
        "curly",
        "coils",
        "braids",
        "bun",
        "ponytail",
        "bald",
        "headscarf"
      ]
    },
    "outfit": {
      "type": "string",
      "enum": [
        "trousers",
        "shorts",
        "skirt",
        "dress",
        "robe"
      ]
    },
    "build": {
      "type": "string",
      "enum": [
        "slight",
        "average",
        "broad"
      ]
    },
    "age": {
      "type": "string",
      "enum": [
        "child",
        "teen",
        "adult",
        "elder"
      ]
    },
    "skin": {
      "type": "string",
      "minLength": 1
    },
    "hair": {
      "type": "string",
      "minLength": 1
    },
    "eyes": {
      "type": "string",
      "minLength": 1
    },
    "shirt": {
      "type": "string",
      "minLength": 1
    },
    "pants": {
      "type": "string",
      "minLength": 1
    },
    "pack": {
      "type": "string",
      "minLength": 1
    },
    "headscarf": {
      "type": "string",
      "minLength": 1
    },
    "accessories": {
      "type": "array",
      "items": {
        "type": "string",
        "enum": [
          "glasses",
          "sunglasses",
          "hat",
          "cap",
          "beanie",
          "scarf",
          "camera",
          "stick"
        ]
      }
    }
  },
  "required": [
    "id"
  ],
  "additionalProperties": false
}

Responses

200

replaced

201

created

400

invalid_request

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

no_such_journal

409

stale_document

413

body_too_large — the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status

/api/v2/{user}/figures/presets

getThe whole vocabulary a figure may be described in, and starting points.

Responses

200

the vocabulary

404

no_such_journal

/api/v2/{user}/figures/preview

getDraw one figure (?figure=) or a party (?party=) as an SVG, so a person can confirm it before it is written.

Responses

200

an SVG drawing

400

nothing_to_draw; invalid_json

404

no_such_journal

/api/v2/{user}/trips/{trip}/travellers/from-photo

postRead a party off a group photograph — proposed, never written. Multipart `photo`, or JSON `{inbox}`/`{gallery}`.

Responses

200

a proposed party, ordered left to right in the photograph

400

expected_photo; invalid_media; invalid_json; unknown_inbox_file; not_this_trip

401

missing_token; invalid_token

402

no_credits

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it; consent_required

404

unknown_trip; helper_unavailable

429

too_many_requests

502

model_failed

/api/v2/{user}/purchases

getThis journal's purchase history, paged.

Responses

200

one page of purchases

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

no_such_journal; credits_disabled

/api/v2/{user}/purchases/{id}

getOne purchase.

Responses

200

the purchase

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

no_such_journal; credits_disabled; unknown_payment

putPropose buying credits at a client-chosen id. Files a pending transaction and mails the owner — grants nothing itself.

Request body

application/json

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "credits": {
      "type": "integer",
      "minimum": 10,
      "maximum": 500
    }
  },
  "required": [
    "credits"
  ],
  "additionalProperties": false
}

Responses

200

the same id was already this exact amount — a no-op re-read

201

created; mail sent

400

invalid_amount

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

no_such_journal; credits_disabled

409

no_owner_address; conflict — the id exists with a different amount

413

body_too_large — the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status

429

too_many_requests

503

no_database

/api/v2/{user}/credits/ledger

getThis journal's credit ledger, paged.

Responses

200

one page of ledger rows

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

no_such_journal; credits_disabled

/api/v2/{user}/contacts/import

postFile many pending contacts at once, from rows a person already agreed.

Request body

application/json

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "rows": {
      "minItems": 1,
      "type": "array",
      "items": {
        "type": "object",
        "propertyNames": {
          "type": "string"
        },
        "additionalProperties": {}
      }
    }
  },
  "required": [
    "rows"
  ],
  "additionalProperties": false
}

Responses

200

per-row outcomes

400

invalid_request

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

no_such_journal

409

contacts_disabled

413

body_too_large — the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status

/api/v2/{user}/contacts/self

postAdd the owner as their own contact, from this journal's own config.json.

Responses

200

added, or already present

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

no_such_journal

409

contacts_disabled; invalid_request

/api/v2/{user}/statements/{src}

getRead a staged bank export as a report — writes nothing.

Responses

200

merchants, payments and rates

400

unreadable_statement

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

no_such_journal; unknown_statement

/api/v2/{user}/postcards/recipients

getWho a postcard may be addressed to — a name, town and country, never a street.

Responses

200

eligible recipients

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

postcards_disabled; no_such_journal

/api/v2/{user}/postcards/texts

getPrefill material for a card's message — every day's opening line, per locale. Send ?trip=.

Responses

200

opening lines by day and locale

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

postcards_disabled; no_such_journal; unknown_trip

/api/v2/{user}/postcards/orders/{id}

getOne postcard order.

Responses

200

the order

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

postcards_disabled; contacts_disabled; no_such_journal; unknown_order

putPropose a postcard order at a client-chosen id. Charges nothing and prints nothing — the owner presses Send.

Request body

application/json

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "source": {
      "anyOf": [
        {
          "type": "object",
          "properties": {
            "trip": {
              "type": "string",
              "minLength": 1
            },
            "day": {
              "type": "string",
              "minLength": 1
            },
            "photo": {
              "type": "string",
              "minLength": 1
            }
          },
          "required": [
            "trip",
            "day",
            "photo"
          ],
          "additionalProperties": false
        },
        {
          "type": "object",
          "properties": {
            "inbox": {
              "type": "string",
              "minLength": 1
            }
          },
          "required": [
            "inbox"
          ],
          "additionalProperties": false
        }
      ]
    },
    "message": {
      "type": "string",
      "minLength": 1,
      "maxLength": 600
    },
    "from": {
      "type": "string",
      "minLength": 1,
      "maxLength": 120
    },
    "recipients": {
      "minItems": 1,
      "maxItems": 25,
      "type": "array",
      "items": {
        "type": "string",
        "minLength": 1
      }
    },
    "locale": {
      "type": "string",
      "minLength": 1
    },
    "crop": {
      "type": "object",
      "properties": {
        "x": {
          "type": "number",
          "minimum": 0,
          "maximum": 1
        },
        "y": {
          "type": "number",
          "minimum": 0,
          "maximum": 1
        },
        "zoom": {
          "type": "number",
          "minimum": 1
        }
      },
      "required": [
        "x",
        "y"
      ],
      "additionalProperties": false
    },
    "figures": {
      "type": "boolean"
    }
  },
  "required": [
    "source",
    "message",
    "from",
    "recipients"
  ],
  "additionalProperties": false
}

Responses

200

the id already exists — echoed back, nothing rewritable through this door

201

proposed

400

invalid_request; test_content

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

postcards_disabled; contacts_disabled; no_such_journal; unknown_photo; unknown_trip; unknown_day; unknown_recipient

409

stale_document

413

body_too_large — the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status

503

no_database

/api/v2/{user}/photobooks/drafts/{trip}

getOne trip's photobook arrangement, whole — every option, and the limits each is checked against.

Responses

200

the arrangement as stored

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

photobook_disabled; no_such_journal; unknown_trip; not_found

putArrange a book. Fields sent land on top of what is stored, or on the defaults; the whole arrangement comes back. Charges nothing, builds nothing, prints nothing — the owner opens `url` to price and buy it.

Request body

application/json

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "options": {
      "type": "object",
      "properties": {
        "size": {
          "type": "string",
          "enum": [
            "pocket",
            "square",
            "portrait",
            "large-square"
          ]
        },
        "coverType": {
          "type": "string",
          "enum": [
            "soft",
            "hard"
          ]
        },
        "locale": {
          "type": "string"
        },
        "excludePhotos": {
          "maxItems": 20000,
          "type": "array",
          "items": {
            "type": "string",
            "maxLength": 300
          }
        },
        "days": {
          "type": "object",
          "propertyNames": {
            "type": "string"
          },
          "additionalProperties": {
            "type": "object",
            "properties": {
              "photos": {
                "maxItems": 500,
                "type": "array",
                "items": {
                  "type": "string",
                  "maxLength": 300
                }
              },
              "keep": {
                "maxItems": 500,
                "type": "array",
                "items": {
                  "type": "string",
                  "maxLength": 300
                }
              },
              "layout": {
                "type": "string",
                "enum": [
                  "auto",
                  "hero",
                  "single",
                  "pair",
                  "grid",
                  "text"
                ]
              },
              "hero": {
                "type": "string",
                "maxLength": 300
              },
              "runOn": {
                "type": "boolean"
              },
              "excluded": {
                "type": "boolean"
              },
              "text": {
                "type": "boolean"
              },
              "textForm": {
                "type": "string",
                "enum": [
                  "runon-auto",
                  "textpage"
                ]
              }
            },
            "additionalProperties": false
          }
        },
        "includeText": {
          "type": "boolean"
        },
        "includeMap": {
          "type": "boolean"
        },
        "mapStyle": {
          "type": "string",
          "enum": [
            "auto",
            "closeup",
            "numbered",
            "chapters",
            "countries",
            "strip"
          ]
        },
        "mapPlaces": {
          "type": "boolean"
        },
        "mapTerrain": {
          "type": "boolean"
        },
        "includeChapters": {
          "type": "boolean"
        },
        "includeNames": {
          "type": "boolean"
        },
        "includeFollowers": {
          "type": "boolean"
        },
        "includeTransport": {
          "type": "boolean"
        },
        "includeCosts": {
          "type": "boolean"
        },
        "includeCharts": {
          "type": "boolean"
        },
        "photoBudget": {
          "type": "string",
          "enum": [
            "highlights",
            "balanced",
            "comprehensive"
          ]
        },
        "includeFigureMarks": {
          "type": "boolean"
        },
        "includeVehicles": {
          "type": "boolean"
        },
        "cover": {
          "type": "string",
          "maxLength": 300
        },
        "spineText": {
          "type": "string",
          "maxLength": 60
        },
        "focalPoints": {
          "type": "object",
          "propertyNames": {
            "type": "string"
          },
          "additionalProperties": {
            "type": "object",
            "properties": {
              "x": {
                "type": "number",
                "minimum": 0,
                "maximum": 1
              },
              "y": {
                "type": "number",
                "minimum": 0,
                "maximum": 1
              }
            },
            "required": [
              "x",
              "y"
            ],
            "additionalProperties": false
          }
        },
        "titleStyle": {
          "type": "string",
          "enum": [
            "journal",
            "magazine",
            "window",
            "tone",
            "places",
            "fullbleed",
            "map",
            "grid"
          ]
        },
        "introOnTitle": {
          "type": "boolean"
        },
        "titlePhoto": {
          "type": "string",
          "maxLength": 300
        },
        "titlePhotos": {
          "maxItems": 4,
          "type": "array",
          "items": {
            "type": "string",
            "maxLength": 300
          }
        },
        "chapterStyle": {
          "type": "string",
          "enum": [
            "stats",
            "typeonly",
            "fullbleed",
            "map"
          ]
        },
        "backStyle": {
          "type": "string",
          "enum": [
            "route",
            "photo",
            "mosaic",
            "quiet"
          ]
        },
        "backPhoto": {
          "type": "string",
          "maxLength": 300
        }
      },
      "additionalProperties": false
    }
  },
  "required": [
    "options"
  ],
  "additionalProperties": false
}

Responses

200

the arrangement, merged and stored

201

the first arrangement for this trip

400

invalid_request

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

photobook_disabled; no_such_journal; unknown_trip

409

stale_document

413

body_too_large — the JSON body is over limits.jsonBodyMaxBytes (4194304 bytes) at GET /api/v2/status

503

no_database

/api/v2/{user}/photobooks/orders/{id}

getOne photobook order. Read-only — building and paying for a book is browser-only.

Responses

200

the order

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

photobook_disabled; no_such_journal; unknown_order

/api/v2/{user}/trips/{trip}/track

postDraw this trip's line from the imported location history — owner only.

Responses

200

segments, points and how many private zones were cut out. written: false means nothing was stored for these dates, and any existing line was left alone

401

missing_token; invalid_token

403

out_of_scope — the token is for a different journal or trip; forbidden — this call is the journal owner's; a trip-scoped token cannot do it

404

unknown_trip

/api/v2/{user}/deletions/{token}

postConfirm a deletion — the button on the mailed page, never something an agent calls.

Responses

200

deleted

404

not_found — no such token for this journal

409

deletion_link_used; deletion_link_expired

410

gone — what it pointed at has already gone

429

too_many_requests

/api/auth/codes

postAsk for a one-time code — one door for four credentials.

Request body

application/json

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "email": {
      "type": "string"
    },
    "phone": {
      "type": "string"
    },
    "for": {
      "type": "string",
      "enum": [
        "read",
        "write",
        "identity",
        "signup"
      ]
    },
    "user": {
      "type": "string"
    },
    "scope": {
      "type": "object",
      "properties": {
        "trip": {
          "type": "string"
        }
      },
      "required": [
        "trip"
      ],
      "additionalProperties": false
    },
    "channel": {
      "type": "string",
      "enum": [
        "mail"
      ]
    },
    "destination": {
      "type": "string"
    },
    "locale": {
      "type": "string"
    }
  },
  "required": [
    "for"
  ],
  "additionalProperties": false
}

Responses

202

accepted — always, whatever the address

400

invalid_request — a shape rule, never address-dependent; invalid_email; sms_unreachable — phone in a country this server's SMS number cannot reach

403

not_authorised — for: "write" to an address that owns nothing and is on no named trip; signup_not_invited

404

signup_disabled; auth_disabled

429

too_many_requests — narrower for write than for read

503

mail_disabled — nothing issued; any code already held is still live; sms_disabled — phone asked for, and this server sends no SMS; mail_failed

/api/auth/codes/redeem

postSpend a code — one door for four credentials.

Request body

application/json

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "email": {
      "type": "string"
    },
    "phone": {
      "type": "string"
    },
    "code": {
      "type": "string"
    },
    "for": {
      "type": "string",
      "enum": [
        "read",
        "write",
        "identity",
        "signup"
      ]
    },
    "user": {
      "type": "string"
    },
    "scope": {
      "type": "object",
      "properties": {
        "trip": {
          "type": "string"
        }
      },
      "required": [
        "trip"
      ],
      "additionalProperties": false
    }
  },
  "required": [
    "code",
    "for"
  ],
  "additionalProperties": false
}

Responses

200

cookie response for "read"/"identity", token response for "write"/"signup"

400

invalid_request

401

invalid_code — wrong, expired, burned, or the wrong for

403

signup_not_invited — the code is not spent

404

signup_disabled; auth_disabled

429

too_many_requests

/api/auth/signup/phone

postProve a telephone number, step one.

Request body

application/json

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "tel": {
      "type": "string"
    },
    "channel": {
      "type": "string",
      "const": "sms"
    }
  },
  "additionalProperties": false
}

Responses

202

code mode: an id to redeem against. Inbound mode: a wa.me link and its prefilled text

400

invalid_request — tel missing or not a number with a country code; sms_unreachable — this server's number cannot reach that number's country

401

invalid_token — missing, invalid, or not step two of signup

403

signup_not_invited

404

signup_disabled; sms_disabled

429

too_many_requests — 3/number/day, 5/address/day, 50/instance/day

503

verification_failed

/api/auth/signup/phone/redeem

postProve a telephone number, step two.

Request body

application/json

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "id": {
      "type": "string"
    },
    "code": {
      "type": "string"
    }
  },
  "required": [
    "id"
  ],
  "additionalProperties": false
}

Responses

200

proven, or the poll status

400

invalid_request — no id sent

401

invalid_token; invalid_code

403

signup_not_invited

404

signup_disabled

429

too_many_requests

/api/auth/handover

postSpend a handover credential for your own 7-day token.

Responses

200

a 7-day agent token, and the status URL to read next

401

missing_token; invalid_handover — expired, already used, revoked, or not a handover credential

404

auth_disabled

/api/auth/{user}/handover

postIssue a handover credential the owner can paste into an agent — owner only.

Responses

200

a 20-minute credential that can only be exchanged, never used to read or write

403

forbidden — a trip-scoped bearer, or a caller that is not this journal's owner

404

auth_disabled

409

no_owner_address

/api/auth/{user}/gps-token

postMint the owner's write:gps token — owner cookie only, refuses even the owner's own agent bearer.

Responses

200

a 30-day token good for POST /api/v2/{user}/import with kind "gps" and dryRun false, and nothing else

403

forbidden — not signed in as this journal's owner in a browser — a bearer token never counts here

404

auth_disabled

409

no_owner_address

/api/auth/{user}/keys

getThe tokens and sessions that can write here — the owner sees every row, anybody else only their own.

Responses

200

one row per live credential this caller may see

403

forbidden — no proven address at all

409

auth_disabled

postRevoke one of them — the owner may revoke any row, anybody else only their own.

Request body

application/json

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "revoke": {
      "type": "string"
    }
  },
  "required": [
    "revoke"
  ],
  "additionalProperties": false
}

Responses

200

revoked

400

invalid_request — no key id sent

403

forbidden — no proven address at all

404

unknown_key — does not exist, or — for a non-owner — belongs to somebody else's address

409

auth_disabled