# Nookivia API - **OpenAPI Version:** `3.2.0` - **API Version:** `1.1.0` ## Introduction The Nookivia API lets your own scripts, and apps you have approved, read and change the trips on your Nookivia account. It speaks JSON over HTTPS. Send requests to the server named at the top of this reference. Every trip route needs a credential; the pages in this section cover what all of them share, and the routes follow. To get started, read Authentication, then try `GET /api/v1/trips`. ## Authentication Every request acts as one **authenticated user**: the person who created the API key, or who approved the app holding the OAuth token. It can only ever see and change that user's trips. Send the credential on every request: ```http Authorization: Bearer ``` ### API keys For your own scripts. An API key (`ak_...`) acts as the user who created it, and holds only the scopes it was created with. Keep it secret, like a password: anyone holding it can do whatever its scopes allow. API keys are not available to create yet. [The developers page](/developers/) will give the steps when they are. ### OAuth For apps that act for other people. An app sends the user to Nookivia to approve it, then exchanges the result for an access token. It gets only the scopes the user approved. Nookivia uses the OAuth 2.0 authorization code flow with PKCE. Apps are public clients and hold no client secret. The authorization and token URLs are in this document's `oauthToken` security scheme. ### Scopes | Scope | Allows | | ------------- | ------------------------------------- | | `trips:read` | See your trips. | | `trips:write` | Create, change and delete your trips. | A route needing a scope the credential does not hold answers 403 `insufficient_scope`, with a challenge naming the missing scope: ```http WWW-Authenticate: Bearer error="insufficient_scope", scope="trips:write" ``` A missing, invalid, expired or revoked credential answers 401 `unauthorized`. ## Rate limits Requests are limited per minute, in two ways: | Limit | Requests per minute | | ---------------------------------------------------- | ------------------- | | Per client address, before the credential is checked | 300 | | Per credential | 120 | Over either limit, a request answers 429 `rate_limited` with a `Retry-After` header giving the seconds to wait. Wait at least that long before trying again. The limits are counted close to where your request enters the network, so treat them as a ceiling rather than an exact quota, and always honour `Retry-After`. The numbers may change. ## Errors Every failure has the same JSON body: ```json { "error": { "code": "invalid_if_match", "message": "If-Match: * is not accepted -- send the version you last read" } } ``` Branch on `code`, not on `message`: `message` is written for a person, names the field it is about, and its wording may change. A 409 `version_conflict` also carries `trip`, the trip as it now stands. | Status | `code` | Meaning | | ------ | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------- | | 400 | `invalid_body` | The body is not JSON, or not a JSON object. | | 400 | `invalid_if_match` | `If-Match` is not a version this API issued. `*` is refused too. | | 400 | `invalid_query` | A query parameter is out of range, unknown, or a `cursor` this API did not issue. | | 400 | `unknown_field` | The body has a key a trip does not have. Nothing was saved. | | 401 | `unauthorized` | No credential, a credential that is invalid, expired or revoked, or one for a user who no longer has an active Nookivia account. | | 403 | `cross_site_request` | A browser sent Nookivia's own sign-in cookie on a change from another website. Send a bearer credential instead. | | 403 | `forbidden` | The credential does not act for a user. | | 403 | `insufficient_scope` | The credential was not granted the scope this route needs. `WWW-Authenticate` names it. | | 404 | `not_found` | No trip with that id on the authenticated user's account. | | 409 | `slug_conflict` | Another trip on the account already uses that slug. Choose another, or leave it out. | | 409 | `version_conflict` | The trip changed since you read it. The body carries the trip as it now stands, with its new `ETag`. | | 413 | `payload_too_large` | The body is larger than 16384 bytes. | | 415 | `unsupported_media_type` | The body is not declared as JSON. Send `Content-Type: application/json`. | | 422 | `validation_failed` | A field broke one of its rules. `message` says which rule, and names the field. | | 428 | `precondition_required` | A change or delete was sent without `If-Match`. | | 429 | `rate_limited` | Too many requests. Wait the number of seconds in `Retry-After`. | New codes may be added within v1. Treat a code you do not recognise by its HTTP status. ## Pagination `GET /api/v1/trips` returns one page at a time: ```json { "trips": [ … ], "nextCursor": "…" } ``` - `limit` sets the page size, from 1 to 100. The default is 50. - Pass `nextCursor` back as `cursor` to get the next page. It is `null` on the last. - The response also carries the next page as a `Link: <…>; rel="next"` header. A cursor is opaque: pass it back unchanged, and do not build or read one. A trip created or deleted while you page never repeats and never goes missing. ## Changing a trip Every trip carries a `version`, which goes up by one on every change. Responses repeat it as a weak `ETag`: ```http ETag: W/"3" ``` To replace (`PUT`), change (`PATCH`) or delete a trip, send that value back as `If-Match`: ```http If-Match: W/"3" ``` - If the trip has changed since, the request answers 409 `version_conflict` with the trip as it now stands. Show the user what changed, then retry with the new `ETag`. - Without `If-Match` it answers 428 `precondition_required`. - `If-Match: *` is refused with 400: a change always names the version it expects, so no edit is ever silently overwritten. `PUT` replaces every settable field; `PATCH` changes only the fields you send. ## Requests and formats - **Bodies** are JSON, sent with `Content-Type: application/json`, and at most 16384 bytes. A key a trip does not have is refused rather than ignored. - **Dates** are whole days, `2027-03-02`, with no time and no zone. - **Timestamps** (`createdAt`, `updatedAt`) are ISO 8601 in UTC with an explicit offset, `2027-03-02T09:30:00+00:00` — never `Z`. - **Ids** are assigned by the server. Address a trip by `id`; its `slug` is for URLs. - **Browsers** can call the API from any website with a bearer credential: responses allow every origin, and expose `ETag`, `Location`, `Retry-After` and `WWW-Authenticate`. ## Versioning The major version is in the path: `/api/v1/...`. Within v1, changes are additive only — new optional fields, new routes, new error codes — so a client should ignore fields and codes it does not know. A breaking change ships as `/api/v2/...` alongside v1, which then carries `Deprecation` and `Sunset` headers until it is withdrawn. ## Tags ### Trips The authenticated user's trips. ## Operations ### List your trips - **Method:** `GET` - **Path:** `/api/v1/trips` - **Operation ID:** `getApiV1Trips` - **Tags:** Trips The authenticated user’s trips in `startDate` order, then by `id`, one page at a time. Follow `nextCursor` -- or the `Link: rel="next"` header -- until it is `null`. A trip created or deleted while you page never repeats and never goes missing. #### Authentication - **apiKey** Scopes: trips:read ``` { "type": "http", "scheme": "bearer", "description": "An API key (`ak_...`), sent as `Authorization: Bearer `. It acts as the user who created it, and holds only the scopes it was created with." } ``` An API key (`ak_...`), sent as `Authorization: Bearer `. It acts as the user who created it, and holds only the scopes it was created with. Or: - **oauthToken** Scopes: trips:read ``` { "type": "http", "scheme": "bearer", "description": "An OAuth 2.0 access token an app obtained with the user’s consent, sent as `Authorization: Bearer `. It holds only the scopes the user granted. The OpenAPI document at /api/openapi.json describes the authorization code flow, with PKCE, and its endpoints." } ``` An OAuth 2.0 access token an app obtained with the user’s consent, sent as `Authorization: Bearer `. It holds only the scopes the user granted. The OpenAPI document at /api/openapi.json describes the authorization code flow, with PKCE, and its endpoints. #### Parameters ##### `limit` - **In:** `query` Trips per page, from 1 to 100. Defaults to 50. `integer`, default: `50`, minimum: `1`, maximum: `100` ##### `cursor` - **In:** `query` The `nextCursor` from the previous page. Opaque: pass it back unchanged, and do not build or read one -- its encoding may change. `string` #### Responses ##### Status: 200 A page of the authenticated user's trips. **Headers:** - **`Link`** The next page, as `<...>; rel="next"` (RFC 8288). Absent on the last page. `string` ###### Content-Type: application/json - **`nextCursor` (required)** `string | null` — Pass as \`cursor\` to get the next page; \`null\` on the last. Opaque -- do not build or parse one. - **`trips` (required)** `array` **Items:** A trip on the authenticated user's account. - **`countries` (required)** `array` **Items:** `string`, pattern: `^[A-Z]{2}$` — A two-letter uppercase ISO country code. - **`createdAt` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?\+00:00$` — An instant in UTC: ISO 8601 ending \`+00:00\`, never \`Z\` and never bare. - **`endDate` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}$` — A whole day, \`2027-03-02\`. Not an instant: it has no time and no zone. - **`id` (required)** `string`, pattern: `^[a-z0-9-]+$` — The trip's API identity: a lowercase ULID assigned by the server. - **`slug` (required)** `string`, pattern: `^[a-z0-9-]+$` — A readable URL segment, unique on the account. Address trips in the API by \`id\`, not by slug. When a write leaves it out, the server derives one from the title and start date. - **`startDate` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}$` — A whole day, \`2027-03-02\`. Not an instant: it has no time and no zone. - **`status` (required)** `string`, possible values: `"booked", "planning"` - **`summary` (required)** `string | null`, maxLength: `500` - **`title` (required)** `string`, minLength: `1`, maxLength: `200` - **`travellers` (required)** `integer`, minimum: `1` — How many people are on the trip. At least one. - **`updatedAt` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?\+00:00$` — An instant in UTC: ISO 8601 ending \`+00:00\`, never \`Z\` and never bare. - **`version` (required)** `integer`, minimum: `1` — Goes up by one on every change. The \`ETag\` header carries it; send that back as \`If-Match\` to change the trip. **Example:** ```json { "trips": [ { "id": "", "slug": "", "version": 1, "title": "", "status": "booked", "startDate": "", "endDate": "", "summary": null, "travellers": 1, "countries": [ "" ], "createdAt": "", "updatedAt": "" } ], "nextCursor": null } ``` ##### Status: 400 A \`limit\` out of range or not a whole number, a \`cursor\` this API did not issue, or a query parameter the list does not take (\`invalid\_query\`). ###### Content-Type: application/json - **`error` (required)** `object` - **`code` (required)** `string`, possible values: `"unauthorized", "not_found", "invalid_body", "unknown_field", "validation_failed", "slug_conflict", "version_conflict", "precondition_required", "invalid_if_match", "forbidden", "insufficient_scope", "cross_site_request", "payload_too_large", "unsupported_media_type", "rate_limited", "invalid_query"` - **`message` (required)** `string` — Written for a person, and names the field it is about. Branch on \`code\`, not on this: its wording may change. **Example:** ```json { "error": { "code": "unauthorized", "message": "" } } ``` ##### Status: 401 No valid credential, or a credential for a user who no longer has an active Nookivia account. Every trip route requires one. ###### Content-Type: application/json - **`error` (required)** `object` - **`code` (required)** `string`, possible values: `"unauthorized", "not_found", "invalid_body", "unknown_field", "validation_failed", "slug_conflict", "version_conflict", "precondition_required", "invalid_if_match", "forbidden", "insufficient_scope", "cross_site_request", "payload_too_large", "unsupported_media_type", "rate_limited", "invalid_query"` - **`message` (required)** `string` — Written for a person, and names the field it is about. Branch on \`code\`, not on this: its wording may change. **Example:** ```json { "error": { "code": "unauthorized", "message": "" } } ``` ##### Status: 403 The credential is valid but may not do this: it was not granted the scope the route needs (\`insufficient\_scope\`, with an RFC 6750 \`WWW-Authenticate\` challenge naming it), or it does not act for a user (\`forbidden\`). **Headers:** - **`WWW-Authenticate`** On `insufficient_scope`: `Bearer error="insufficient_scope", scope="…"`. `string` ###### Content-Type: application/json - **`error` (required)** `object` - **`code` (required)** `string`, possible values: `"unauthorized", "not_found", "invalid_body", "unknown_field", "validation_failed", "slug_conflict", "version_conflict", "precondition_required", "invalid_if_match", "forbidden", "insufficient_scope", "cross_site_request", "payload_too_large", "unsupported_media_type", "rate_limited", "invalid_query"` - **`message` (required)** `string` — Written for a person, and names the field it is about. Branch on \`code\`, not on this: its wording may change. **Example:** ```json { "error": { "code": "unauthorized", "message": "" } } ``` ##### Status: 429 Too many requests, from this address or on this credential. Limits are per minute; wait the number of seconds in \`Retry-After\`. **Headers:** - **`Retry-After`** Seconds to wait before trying again. `integer`, minimum: `1` ###### Content-Type: application/json - **`error` (required)** `object` - **`code` (required)** `string`, possible values: `"unauthorized", "not_found", "invalid_body", "unknown_field", "validation_failed", "slug_conflict", "version_conflict", "precondition_required", "invalid_if_match", "forbidden", "insufficient_scope", "cross_site_request", "payload_too_large", "unsupported_media_type", "rate_limited", "invalid_query"` - **`message` (required)** `string` — Written for a person, and names the field it is about. Branch on \`code\`, not on this: its wording may change. **Example:** ```json { "error": { "code": "unauthorized", "message": "" } } ``` ### Create a trip - **Method:** `POST` - **Path:** `/api/v1/trips` - **Operation ID:** `postApiV1Trips` - **Tags:** Trips The server assigns `id`, `version` (1) and, unless you send one, `slug` -- derived from the title and start date. #### Authentication - **apiKey** Scopes: trips:write ``` { "type": "http", "scheme": "bearer", "description": "An API key (`ak_...`), sent as `Authorization: Bearer `. It acts as the user who created it, and holds only the scopes it was created with." } ``` An API key (`ak_...`), sent as `Authorization: Bearer `. It acts as the user who created it, and holds only the scopes it was created with. Or: - **oauthToken** Scopes: trips:write ``` { "type": "http", "scheme": "bearer", "description": "An OAuth 2.0 access token an app obtained with the user’s consent, sent as `Authorization: Bearer `. It holds only the scopes the user granted. The OpenAPI document at /api/openapi.json describes the authorization code flow, with PKCE, and its endpoints." } ``` An OAuth 2.0 access token an app obtained with the user’s consent, sent as `Authorization: Bearer `. It holds only the scopes the user granted. The OpenAPI document at /api/openapi.json describes the authorization code flow, with PKCE, and its endpoints. #### Request Body **Required:** `true` ##### Content-Type: application/json - **`endDate` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}$` — A whole day, \`2027-03-02\`. Not an instant: it has no time and no zone. - **`startDate` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}$` — A whole day, \`2027-03-02\`. Not an instant: it has no time and no zone. - **`status` (required)** `string`, possible values: `"booked", "planning"` - **`title` (required)** `string`, minLength: `1`, maxLength: `200` - **`countries`** `array` — Every country the trip visits, in the order the itinerary meets them. **Items:** `string`, pattern: `^[A-Z]{2}$` — A two-letter uppercase ISO country code. - Min items: `1` - **`slug`** `string`, pattern: `^[a-z0-9-]+$` — A readable URL segment, unique on the account. Address trips in the API by \`id\`, not by slug. When a write leaves it out, the server derives one from the title and start date. - **`summary`** `string | null`, maxLength: `500` - **`travellers`** `integer`, minimum: `1` — How many people are on the trip. At least one. **Additional properties:** never (false schema) **Example:** ```json { "title": "", "status": "booked", "startDate": "", "endDate": "", "summary": null, "travellers": 1, "countries": [ "" ], "slug": "" } ``` #### Responses ##### Status: 201 Created. **Headers:** - **`ETag`** The trip's version, weak-validated. Send it as `If-Match` to write. `string`, pattern: `^W/"\d+"$` - **`Location`** The new trip. `string` ###### Content-Type: application/json A trip on the authenticated user's account. - **`countries` (required)** `array` **Items:** `string`, pattern: `^[A-Z]{2}$` — A two-letter uppercase ISO country code. - **`createdAt` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?\+00:00$` — An instant in UTC: ISO 8601 ending \`+00:00\`, never \`Z\` and never bare. - **`endDate` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}$` — A whole day, \`2027-03-02\`. Not an instant: it has no time and no zone. - **`id` (required)** `string`, pattern: `^[a-z0-9-]+$` — The trip's API identity: a lowercase ULID assigned by the server. - **`slug` (required)** `string`, pattern: `^[a-z0-9-]+$` — A readable URL segment, unique on the account. Address trips in the API by \`id\`, not by slug. When a write leaves it out, the server derives one from the title and start date. - **`startDate` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}$` — A whole day, \`2027-03-02\`. Not an instant: it has no time and no zone. - **`status` (required)** `string`, possible values: `"booked", "planning"` - **`summary` (required)** `string | null`, maxLength: `500` - **`title` (required)** `string`, minLength: `1`, maxLength: `200` - **`travellers` (required)** `integer`, minimum: `1` — How many people are on the trip. At least one. - **`updatedAt` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?\+00:00$` — An instant in UTC: ISO 8601 ending \`+00:00\`, never \`Z\` and never bare. - **`version` (required)** `integer`, minimum: `1` — Goes up by one on every change. The \`ETag\` header carries it; send that back as \`If-Match\` to change the trip. **Example:** ```json { "id": "", "slug": "", "version": 1, "title": "", "status": "booked", "startDate": "", "endDate": "", "summary": null, "travellers": 1, "countries": [ "" ], "createdAt": "", "updatedAt": "" } ``` ##### Status: 400 Not JSON, not an object, or a key a trip does not have (\`unknown\_field\`). ###### Content-Type: application/json - **`error` (required)** `object` - **`code` (required)** `string`, possible values: `"unauthorized", "not_found", "invalid_body", "unknown_field", "validation_failed", "slug_conflict", "version_conflict", "precondition_required", "invalid_if_match", "forbidden", "insufficient_scope", "cross_site_request", "payload_too_large", "unsupported_media_type", "rate_limited", "invalid_query"` - **`message` (required)** `string` — Written for a person, and names the field it is about. Branch on \`code\`, not on this: its wording may change. **Example:** ```json { "error": { "code": "unauthorized", "message": "" } } ``` ##### Status: 401 No valid credential, or a credential for a user who no longer has an active Nookivia account. Every trip route requires one. ###### Content-Type: application/json - **`error` (required)** `object` - **`code` (required)** `string`, possible values: `"unauthorized", "not_found", "invalid_body", "unknown_field", "validation_failed", "slug_conflict", "version_conflict", "precondition_required", "invalid_if_match", "forbidden", "insufficient_scope", "cross_site_request", "payload_too_large", "unsupported_media_type", "rate_limited", "invalid_query"` - **`message` (required)** `string` — Written for a person, and names the field it is about. Branch on \`code\`, not on this: its wording may change. **Example:** ```json { "error": { "code": "unauthorized", "message": "" } } ``` ##### Status: 403 The credential is valid but may not do this: it was not granted the scope the route needs (\`insufficient\_scope\`, with an RFC 6750 \`WWW-Authenticate\` challenge naming it), or it does not act for a user (\`forbidden\`). **Headers:** - **`WWW-Authenticate`** On `insufficient_scope`: `Bearer error="insufficient_scope", scope="…"`. `string` ###### Content-Type: application/json - **`error` (required)** `object` - **`code` (required)** `string`, possible values: `"unauthorized", "not_found", "invalid_body", "unknown_field", "validation_failed", "slug_conflict", "version_conflict", "precondition_required", "invalid_if_match", "forbidden", "insufficient_scope", "cross_site_request", "payload_too_large", "unsupported_media_type", "rate_limited", "invalid_query"` - **`message` (required)** `string` — Written for a person, and names the field it is about. Branch on \`code\`, not on this: its wording may change. **Example:** ```json { "error": { "code": "unauthorized", "message": "" } } ``` ##### Status: 409 The slug you chose is already used by another trip on the account. ###### Content-Type: application/json - **`error` (required)** `object` - **`code` (required)** `string`, possible values: `"unauthorized", "not_found", "invalid_body", "unknown_field", "validation_failed", "slug_conflict", "version_conflict", "precondition_required", "invalid_if_match", "forbidden", "insufficient_scope", "cross_site_request", "payload_too_large", "unsupported_media_type", "rate_limited", "invalid_query"` - **`message` (required)** `string` — Written for a person, and names the field it is about. Branch on \`code\`, not on this: its wording may change. **Example:** ```json { "error": { "code": "unauthorized", "message": "" } } ``` ##### Status: 413 The body is larger than 16384 bytes (\`payload\_too\_large\`). ###### Content-Type: application/json - **`error` (required)** `object` - **`code` (required)** `string`, possible values: `"unauthorized", "not_found", "invalid_body", "unknown_field", "validation_failed", "slug_conflict", "version_conflict", "precondition_required", "invalid_if_match", "forbidden", "insufficient_scope", "cross_site_request", "payload_too_large", "unsupported_media_type", "rate_limited", "invalid_query"` - **`message` (required)** `string` — Written for a person, and names the field it is about. Branch on \`code\`, not on this: its wording may change. **Example:** ```json { "error": { "code": "unauthorized", "message": "" } } ``` ##### Status: 415 The body is not declared as JSON (\`unsupported\_media\_type\`). Send \`Content-Type: application/json\`. ###### Content-Type: application/json - **`error` (required)** `object` - **`code` (required)** `string`, possible values: `"unauthorized", "not_found", "invalid_body", "unknown_field", "validation_failed", "slug_conflict", "version_conflict", "precondition_required", "invalid_if_match", "forbidden", "insufficient_scope", "cross_site_request", "payload_too_large", "unsupported_media_type", "rate_limited", "invalid_query"` - **`message` (required)** `string` — Written for a person, and names the field it is about. Branch on \`code\`, not on this: its wording may change. **Example:** ```json { "error": { "code": "unauthorized", "message": "" } } ``` ##### Status: 422 A field broke one of its rules. \`message\` says which rule, and names the field. ###### Content-Type: application/json - **`error` (required)** `object` - **`code` (required)** `string`, possible values: `"unauthorized", "not_found", "invalid_body", "unknown_field", "validation_failed", "slug_conflict", "version_conflict", "precondition_required", "invalid_if_match", "forbidden", "insufficient_scope", "cross_site_request", "payload_too_large", "unsupported_media_type", "rate_limited", "invalid_query"` - **`message` (required)** `string` — Written for a person, and names the field it is about. Branch on \`code\`, not on this: its wording may change. **Example:** ```json { "error": { "code": "unauthorized", "message": "" } } ``` ##### Status: 429 Too many requests, from this address or on this credential. Limits are per minute; wait the number of seconds in \`Retry-After\`. **Headers:** - **`Retry-After`** Seconds to wait before trying again. `integer`, minimum: `1` ###### Content-Type: application/json - **`error` (required)** `object` - **`code` (required)** `string`, possible values: `"unauthorized", "not_found", "invalid_body", "unknown_field", "validation_failed", "slug_conflict", "version_conflict", "precondition_required", "invalid_if_match", "forbidden", "insufficient_scope", "cross_site_request", "payload_too_large", "unsupported_media_type", "rate_limited", "invalid_query"` - **`message` (required)** `string` — Written for a person, and names the field it is about. Branch on \`code\`, not on this: its wording may change. **Example:** ```json { "error": { "code": "unauthorized", "message": "" } } ``` ### Read a trip - **Method:** `GET` - **Path:** `/api/v1/trips/{id}` - **Operation ID:** `getApiV1TripsById` - **Tags:** Trips #### Authentication - **apiKey** Scopes: trips:read ``` { "type": "http", "scheme": "bearer", "description": "An API key (`ak_...`), sent as `Authorization: Bearer `. It acts as the user who created it, and holds only the scopes it was created with." } ``` An API key (`ak_...`), sent as `Authorization: Bearer `. It acts as the user who created it, and holds only the scopes it was created with. Or: - **oauthToken** Scopes: trips:read ``` { "type": "http", "scheme": "bearer", "description": "An OAuth 2.0 access token an app obtained with the user’s consent, sent as `Authorization: Bearer `. It holds only the scopes the user granted. The OpenAPI document at /api/openapi.json describes the authorization code flow, with PKCE, and its endpoints." } ``` An OAuth 2.0 access token an app obtained with the user’s consent, sent as `Authorization: Bearer `. It holds only the scopes the user granted. The OpenAPI document at /api/openapi.json describes the authorization code flow, with PKCE, and its endpoints. #### Parameters ##### `id` required - **In:** `path` The trip id. A value that could not be a trip id answers 404. `string`, pattern: `^[a-z0-9-]+$` #### Responses ##### Status: 200 The trip. **Headers:** - **`ETag`** The trip's version, weak-validated. Send it as `If-Match` to write. `string`, pattern: `^W/"\d+"$` ###### Content-Type: application/json A trip on the authenticated user's account. - **`countries` (required)** `array` **Items:** `string`, pattern: `^[A-Z]{2}$` — A two-letter uppercase ISO country code. - **`createdAt` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?\+00:00$` — An instant in UTC: ISO 8601 ending \`+00:00\`, never \`Z\` and never bare. - **`endDate` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}$` — A whole day, \`2027-03-02\`. Not an instant: it has no time and no zone. - **`id` (required)** `string`, pattern: `^[a-z0-9-]+$` — The trip's API identity: a lowercase ULID assigned by the server. - **`slug` (required)** `string`, pattern: `^[a-z0-9-]+$` — A readable URL segment, unique on the account. Address trips in the API by \`id\`, not by slug. When a write leaves it out, the server derives one from the title and start date. - **`startDate` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}$` — A whole day, \`2027-03-02\`. Not an instant: it has no time and no zone. - **`status` (required)** `string`, possible values: `"booked", "planning"` - **`summary` (required)** `string | null`, maxLength: `500` - **`title` (required)** `string`, minLength: `1`, maxLength: `200` - **`travellers` (required)** `integer`, minimum: `1` — How many people are on the trip. At least one. - **`updatedAt` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?\+00:00$` — An instant in UTC: ISO 8601 ending \`+00:00\`, never \`Z\` and never bare. - **`version` (required)** `integer`, minimum: `1` — Goes up by one on every change. The \`ETag\` header carries it; send that back as \`If-Match\` to change the trip. **Example:** ```json { "id": "", "slug": "", "version": 1, "title": "", "status": "booked", "startDate": "", "endDate": "", "summary": null, "travellers": 1, "countries": [ "" ], "createdAt": "", "updatedAt": "" } ``` ##### Status: 401 No valid credential, or a credential for a user who no longer has an active Nookivia account. Every trip route requires one. ###### Content-Type: application/json - **`error` (required)** `object` - **`code` (required)** `string`, possible values: `"unauthorized", "not_found", "invalid_body", "unknown_field", "validation_failed", "slug_conflict", "version_conflict", "precondition_required", "invalid_if_match", "forbidden", "insufficient_scope", "cross_site_request", "payload_too_large", "unsupported_media_type", "rate_limited", "invalid_query"` - **`message` (required)** `string` — Written for a person, and names the field it is about. Branch on \`code\`, not on this: its wording may change. **Example:** ```json { "error": { "code": "unauthorized", "message": "" } } ``` ##### Status: 403 The credential is valid but may not do this: it was not granted the scope the route needs (\`insufficient\_scope\`, with an RFC 6750 \`WWW-Authenticate\` challenge naming it), or it does not act for a user (\`forbidden\`). **Headers:** - **`WWW-Authenticate`** On `insufficient_scope`: `Bearer error="insufficient_scope", scope="…"`. `string` ###### Content-Type: application/json - **`error` (required)** `object` - **`code` (required)** `string`, possible values: `"unauthorized", "not_found", "invalid_body", "unknown_field", "validation_failed", "slug_conflict", "version_conflict", "precondition_required", "invalid_if_match", "forbidden", "insufficient_scope", "cross_site_request", "payload_too_large", "unsupported_media_type", "rate_limited", "invalid_query"` - **`message` (required)** `string` — Written for a person, and names the field it is about. Branch on \`code\`, not on this: its wording may change. **Example:** ```json { "error": { "code": "unauthorized", "message": "" } } ``` ##### Status: 404 No trip with that id on the authenticated user's account. ###### Content-Type: application/json - **`error` (required)** `object` - **`code` (required)** `string`, possible values: `"unauthorized", "not_found", "invalid_body", "unknown_field", "validation_failed", "slug_conflict", "version_conflict", "precondition_required", "invalid_if_match", "forbidden", "insufficient_scope", "cross_site_request", "payload_too_large", "unsupported_media_type", "rate_limited", "invalid_query"` - **`message` (required)** `string` — Written for a person, and names the field it is about. Branch on \`code\`, not on this: its wording may change. **Example:** ```json { "error": { "code": "unauthorized", "message": "" } } ``` ##### Status: 429 Too many requests, from this address or on this credential. Limits are per minute; wait the number of seconds in \`Retry-After\`. **Headers:** - **`Retry-After`** Seconds to wait before trying again. `integer`, minimum: `1` ###### Content-Type: application/json - **`error` (required)** `object` - **`code` (required)** `string`, possible values: `"unauthorized", "not_found", "invalid_body", "unknown_field", "validation_failed", "slug_conflict", "version_conflict", "precondition_required", "invalid_if_match", "forbidden", "insufficient_scope", "cross_site_request", "payload_too_large", "unsupported_media_type", "rate_limited", "invalid_query"` - **`message` (required)** `string` — Written for a person, and names the field it is about. Branch on \`code\`, not on this: its wording may change. **Example:** ```json { "error": { "code": "unauthorized", "message": "" } } ``` ### Replace a trip - **Method:** `PUT` - **Path:** `/api/v1/trips/{id}` - **Operation ID:** `putApiV1TripsById` - **Tags:** Trips Replaces every settable field. Send the trip’s `ETag` as `If-Match`: if the trip has changed since you read it, the write answers 409 rather than overwriting. #### Authentication - **apiKey** Scopes: trips:write ``` { "type": "http", "scheme": "bearer", "description": "An API key (`ak_...`), sent as `Authorization: Bearer `. It acts as the user who created it, and holds only the scopes it was created with." } ``` An API key (`ak_...`), sent as `Authorization: Bearer `. It acts as the user who created it, and holds only the scopes it was created with. Or: - **oauthToken** Scopes: trips:write ``` { "type": "http", "scheme": "bearer", "description": "An OAuth 2.0 access token an app obtained with the user’s consent, sent as `Authorization: Bearer `. It holds only the scopes the user granted. The OpenAPI document at /api/openapi.json describes the authorization code flow, with PKCE, and its endpoints." } ``` An OAuth 2.0 access token an app obtained with the user’s consent, sent as `Authorization: Bearer `. It holds only the scopes the user granted. The OpenAPI document at /api/openapi.json describes the authorization code flow, with PKCE, and its endpoints. #### Parameters ##### `id` required - **In:** `path` The trip id. A value that could not be a trip id answers 404. `string`, pattern: `^[a-z0-9-]+$` ##### `If-Match` required - **In:** `header` The trip's `ETag` from your last read. Required: without it the request answers 428. `*` is refused with 400, because a change always names the version it expects. `string` #### Request Body **Required:** `true` ##### Content-Type: application/json - **`endDate` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}$` — A whole day, \`2027-03-02\`. Not an instant: it has no time and no zone. - **`startDate` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}$` — A whole day, \`2027-03-02\`. Not an instant: it has no time and no zone. - **`status` (required)** `string`, possible values: `"booked", "planning"` - **`title` (required)** `string`, minLength: `1`, maxLength: `200` - **`countries`** `array` — Every country the trip visits, in the order the itinerary meets them. **Items:** `string`, pattern: `^[A-Z]{2}$` — A two-letter uppercase ISO country code. - Min items: `1` - **`slug`** `string`, pattern: `^[a-z0-9-]+$` — A readable URL segment, unique on the account. Address trips in the API by \`id\`, not by slug. When a write leaves it out, the server derives one from the title and start date. - **`summary`** `string | null`, maxLength: `500` - **`travellers`** `integer`, minimum: `1` — How many people are on the trip. At least one. **Additional properties:** never (false schema) **Example:** ```json { "title": "", "status": "booked", "startDate": "", "endDate": "", "summary": null, "travellers": 1, "countries": [ "" ], "slug": "" } ``` #### Responses ##### Status: 200 The trip. **Headers:** - **`ETag`** The trip's version, weak-validated. Send it as `If-Match` to write. `string`, pattern: `^W/"\d+"$` ###### Content-Type: application/json A trip on the authenticated user's account. - **`countries` (required)** `array` **Items:** `string`, pattern: `^[A-Z]{2}$` — A two-letter uppercase ISO country code. - **`createdAt` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?\+00:00$` — An instant in UTC: ISO 8601 ending \`+00:00\`, never \`Z\` and never bare. - **`endDate` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}$` — A whole day, \`2027-03-02\`. Not an instant: it has no time and no zone. - **`id` (required)** `string`, pattern: `^[a-z0-9-]+$` — The trip's API identity: a lowercase ULID assigned by the server. - **`slug` (required)** `string`, pattern: `^[a-z0-9-]+$` — A readable URL segment, unique on the account. Address trips in the API by \`id\`, not by slug. When a write leaves it out, the server derives one from the title and start date. - **`startDate` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}$` — A whole day, \`2027-03-02\`. Not an instant: it has no time and no zone. - **`status` (required)** `string`, possible values: `"booked", "planning"` - **`summary` (required)** `string | null`, maxLength: `500` - **`title` (required)** `string`, minLength: `1`, maxLength: `200` - **`travellers` (required)** `integer`, minimum: `1` — How many people are on the trip. At least one. - **`updatedAt` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?\+00:00$` — An instant in UTC: ISO 8601 ending \`+00:00\`, never \`Z\` and never bare. - **`version` (required)** `integer`, minimum: `1` — Goes up by one on every change. The \`ETag\` header carries it; send that back as \`If-Match\` to change the trip. **Example:** ```json { "id": "", "slug": "", "version": 1, "title": "", "status": "booked", "startDate": "", "endDate": "", "summary": null, "travellers": 1, "countries": [ "" ], "createdAt": "", "updatedAt": "" } ``` ##### Status: 400 A malformed body (\`invalid\_body\`, \`unknown\_field\`) or an \`If-Match\` that is not a version, including \`\*\` (\`invalid\_if\_match\`). ###### Content-Type: application/json - **`error` (required)** `object` - **`code` (required)** `string`, possible values: `"unauthorized", "not_found", "invalid_body", "unknown_field", "validation_failed", "slug_conflict", "version_conflict", "precondition_required", "invalid_if_match", "forbidden", "insufficient_scope", "cross_site_request", "payload_too_large", "unsupported_media_type", "rate_limited", "invalid_query"` - **`message` (required)** `string` — Written for a person, and names the field it is about. Branch on \`code\`, not on this: its wording may change. **Example:** ```json { "error": { "code": "unauthorized", "message": "" } } ``` ##### Status: 401 No valid credential, or a credential for a user who no longer has an active Nookivia account. Every trip route requires one. ###### Content-Type: application/json - **`error` (required)** `object` - **`code` (required)** `string`, possible values: `"unauthorized", "not_found", "invalid_body", "unknown_field", "validation_failed", "slug_conflict", "version_conflict", "precondition_required", "invalid_if_match", "forbidden", "insufficient_scope", "cross_site_request", "payload_too_large", "unsupported_media_type", "rate_limited", "invalid_query"` - **`message` (required)** `string` — Written for a person, and names the field it is about. Branch on \`code\`, not on this: its wording may change. **Example:** ```json { "error": { "code": "unauthorized", "message": "" } } ``` ##### Status: 403 The credential is valid but may not do this: it was not granted the scope the route needs (\`insufficient\_scope\`, with an RFC 6750 \`WWW-Authenticate\` challenge naming it), or it does not act for a user (\`forbidden\`). **Headers:** - **`WWW-Authenticate`** On `insufficient_scope`: `Bearer error="insufficient_scope", scope="…"`. `string` ###### Content-Type: application/json - **`error` (required)** `object` - **`code` (required)** `string`, possible values: `"unauthorized", "not_found", "invalid_body", "unknown_field", "validation_failed", "slug_conflict", "version_conflict", "precondition_required", "invalid_if_match", "forbidden", "insufficient_scope", "cross_site_request", "payload_too_large", "unsupported_media_type", "rate_limited", "invalid_query"` - **`message` (required)** `string` — Written for a person, and names the field it is about. Branch on \`code\`, not on this: its wording may change. **Example:** ```json { "error": { "code": "unauthorized", "message": "" } } ``` ##### Status: 404 No trip with that id on the authenticated user's account. ###### Content-Type: application/json - **`error` (required)** `object` - **`code` (required)** `string`, possible values: `"unauthorized", "not_found", "invalid_body", "unknown_field", "validation_failed", "slug_conflict", "version_conflict", "precondition_required", "invalid_if_match", "forbidden", "insufficient_scope", "cross_site_request", "payload_too_large", "unsupported_media_type", "rate_limited", "invalid_query"` - **`message` (required)** `string` — Written for a person, and names the field it is about. Branch on \`code\`, not on this: its wording may change. **Example:** ```json { "error": { "code": "unauthorized", "message": "" } } ``` ##### Status: 409 The trip changed since you last read it (\`version\_conflict\`, which carries the trip as it now stands so you can show what changed), or the slug you chose is taken (\`slug\_conflict\`). **Headers:** - **`ETag`** The trip's version, weak-validated. Send it as `If-Match` to write. `string`, pattern: `^W/"\d+"$` ###### Content-Type: application/json \`trip\` is present when \`error.code\` is \`version\_conflict\`, carrying the trip as it now stands, and absent when it is \`slug\_conflict\`. - **`error` (required)** `object` - **`code` (required)** `string`, possible values: `"unauthorized", "not_found", "invalid_body", "unknown_field", "validation_failed", "slug_conflict", "version_conflict", "precondition_required", "invalid_if_match", "forbidden", "insufficient_scope", "cross_site_request", "payload_too_large", "unsupported_media_type", "rate_limited", "invalid_query"` - **`message` (required)** `string` — Written for a person, and names the field it is about. Branch on \`code\`, not on this: its wording may change. - **`trip`** `object` — A trip on the authenticated user's account. - **`countries` (required)** `array` **Items:** `string`, pattern: `^[A-Z]{2}$` — A two-letter uppercase ISO country code. - **`createdAt` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?\+00:00$` — An instant in UTC: ISO 8601 ending \`+00:00\`, never \`Z\` and never bare. - **`endDate` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}$` — A whole day, \`2027-03-02\`. Not an instant: it has no time and no zone. - **`id` (required)** `string`, pattern: `^[a-z0-9-]+$` — The trip's API identity: a lowercase ULID assigned by the server. - **`slug` (required)** `string`, pattern: `^[a-z0-9-]+$` — A readable URL segment, unique on the account. Address trips in the API by \`id\`, not by slug. When a write leaves it out, the server derives one from the title and start date. - **`startDate` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}$` — A whole day, \`2027-03-02\`. Not an instant: it has no time and no zone. - **`status` (required)** `string`, possible values: `"booked", "planning"` - **`summary` (required)** `string | null`, maxLength: `500` - **`title` (required)** `string`, minLength: `1`, maxLength: `200` - **`travellers` (required)** `integer`, minimum: `1` — How many people are on the trip. At least one. - **`updatedAt` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?\+00:00$` — An instant in UTC: ISO 8601 ending \`+00:00\`, never \`Z\` and never bare. - **`version` (required)** `integer`, minimum: `1` — Goes up by one on every change. The \`ETag\` header carries it; send that back as \`If-Match\` to change the trip. **Example:** ```json { "error": { "code": "unauthorized", "message": "" }, "trip": { "id": "", "slug": "", "version": 1, "title": "", "status": "booked", "startDate": "", "endDate": "", "summary": null, "travellers": 1, "countries": [ "" ], "createdAt": "", "updatedAt": "" } } ``` ##### Status: 413 The body is larger than 16384 bytes (\`payload\_too\_large\`). ###### Content-Type: application/json - **`error` (required)** `object` - **`code` (required)** `string`, possible values: `"unauthorized", "not_found", "invalid_body", "unknown_field", "validation_failed", "slug_conflict", "version_conflict", "precondition_required", "invalid_if_match", "forbidden", "insufficient_scope", "cross_site_request", "payload_too_large", "unsupported_media_type", "rate_limited", "invalid_query"` - **`message` (required)** `string` — Written for a person, and names the field it is about. Branch on \`code\`, not on this: its wording may change. **Example:** ```json { "error": { "code": "unauthorized", "message": "" } } ``` ##### Status: 415 The body is not declared as JSON (\`unsupported\_media\_type\`). Send \`Content-Type: application/json\`. ###### Content-Type: application/json - **`error` (required)** `object` - **`code` (required)** `string`, possible values: `"unauthorized", "not_found", "invalid_body", "unknown_field", "validation_failed", "slug_conflict", "version_conflict", "precondition_required", "invalid_if_match", "forbidden", "insufficient_scope", "cross_site_request", "payload_too_large", "unsupported_media_type", "rate_limited", "invalid_query"` - **`message` (required)** `string` — Written for a person, and names the field it is about. Branch on \`code\`, not on this: its wording may change. **Example:** ```json { "error": { "code": "unauthorized", "message": "" } } ``` ##### Status: 422 A field broke one of its rules. \`message\` says which rule, and names the field. ###### Content-Type: application/json - **`error` (required)** `object` - **`code` (required)** `string`, possible values: `"unauthorized", "not_found", "invalid_body", "unknown_field", "validation_failed", "slug_conflict", "version_conflict", "precondition_required", "invalid_if_match", "forbidden", "insufficient_scope", "cross_site_request", "payload_too_large", "unsupported_media_type", "rate_limited", "invalid_query"` - **`message` (required)** `string` — Written for a person, and names the field it is about. Branch on \`code\`, not on this: its wording may change. **Example:** ```json { "error": { "code": "unauthorized", "message": "" } } ``` ##### Status: 428 No \`If-Match\`. Send the version you last read. ###### Content-Type: application/json - **`error` (required)** `object` - **`code` (required)** `string`, possible values: `"unauthorized", "not_found", "invalid_body", "unknown_field", "validation_failed", "slug_conflict", "version_conflict", "precondition_required", "invalid_if_match", "forbidden", "insufficient_scope", "cross_site_request", "payload_too_large", "unsupported_media_type", "rate_limited", "invalid_query"` - **`message` (required)** `string` — Written for a person, and names the field it is about. Branch on \`code\`, not on this: its wording may change. **Example:** ```json { "error": { "code": "unauthorized", "message": "" } } ``` ##### Status: 429 Too many requests, from this address or on this credential. Limits are per minute; wait the number of seconds in \`Retry-After\`. **Headers:** - **`Retry-After`** Seconds to wait before trying again. `integer`, minimum: `1` ###### Content-Type: application/json - **`error` (required)** `object` - **`code` (required)** `string`, possible values: `"unauthorized", "not_found", "invalid_body", "unknown_field", "validation_failed", "slug_conflict", "version_conflict", "precondition_required", "invalid_if_match", "forbidden", "insufficient_scope", "cross_site_request", "payload_too_large", "unsupported_media_type", "rate_limited", "invalid_query"` - **`message` (required)** `string` — Written for a person, and names the field it is about. Branch on \`code\`, not on this: its wording may change. **Example:** ```json { "error": { "code": "unauthorized", "message": "" } } ``` ### Change some of a trip - **Method:** `PATCH` - **Path:** `/api/v1/trips/{id}` - **Operation ID:** `patchApiV1TripsById` - **Tags:** Trips Changes only the fields you send. Send the trip’s `ETag` as `If-Match`: if the trip has changed since you read it, the write answers 409 rather than merging over the other change. #### Authentication - **apiKey** Scopes: trips:write ``` { "type": "http", "scheme": "bearer", "description": "An API key (`ak_...`), sent as `Authorization: Bearer `. It acts as the user who created it, and holds only the scopes it was created with." } ``` An API key (`ak_...`), sent as `Authorization: Bearer `. It acts as the user who created it, and holds only the scopes it was created with. Or: - **oauthToken** Scopes: trips:write ``` { "type": "http", "scheme": "bearer", "description": "An OAuth 2.0 access token an app obtained with the user’s consent, sent as `Authorization: Bearer `. It holds only the scopes the user granted. The OpenAPI document at /api/openapi.json describes the authorization code flow, with PKCE, and its endpoints." } ``` An OAuth 2.0 access token an app obtained with the user’s consent, sent as `Authorization: Bearer `. It holds only the scopes the user granted. The OpenAPI document at /api/openapi.json describes the authorization code flow, with PKCE, and its endpoints. #### Parameters ##### `id` required - **In:** `path` The trip id. A value that could not be a trip id answers 404. `string`, pattern: `^[a-z0-9-]+$` ##### `If-Match` required - **In:** `header` The trip's `ETag` from your last read. Required: without it the request answers 428. `*` is refused with 400, because a change always names the version it expects. `string` #### Request Body Any subset of the settable fields. **Required:** `true` ##### Content-Type: application/json - **`endDate` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}$` — A whole day, \`2027-03-02\`. Not an instant: it has no time and no zone. - **`startDate` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}$` — A whole day, \`2027-03-02\`. Not an instant: it has no time and no zone. - **`status` (required)** `string`, possible values: `"booked", "planning"` - **`title` (required)** `string`, minLength: `1`, maxLength: `200` - **`countries`** `array` — Every country the trip visits, in the order the itinerary meets them. **Items:** `string`, pattern: `^[A-Z]{2}$` — A two-letter uppercase ISO country code. - Min items: `1` - **`slug`** `string`, pattern: `^[a-z0-9-]+$` — A readable URL segment, unique on the account. Address trips in the API by \`id\`, not by slug. When a write leaves it out, the server derives one from the title and start date. - **`summary`** `string | null`, maxLength: `500` - **`travellers`** `integer`, minimum: `1` — How many people are on the trip. At least one. **Additional properties:** never (false schema) **Example:** ```json { "title": "", "status": "booked", "startDate": "", "endDate": "", "summary": null, "travellers": 1, "countries": [ "" ], "slug": "" } ``` #### Responses ##### Status: 200 The trip. **Headers:** - **`ETag`** The trip's version, weak-validated. Send it as `If-Match` to write. `string`, pattern: `^W/"\d+"$` ###### Content-Type: application/json A trip on the authenticated user's account. - **`countries` (required)** `array` **Items:** `string`, pattern: `^[A-Z]{2}$` — A two-letter uppercase ISO country code. - **`createdAt` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?\+00:00$` — An instant in UTC: ISO 8601 ending \`+00:00\`, never \`Z\` and never bare. - **`endDate` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}$` — A whole day, \`2027-03-02\`. Not an instant: it has no time and no zone. - **`id` (required)** `string`, pattern: `^[a-z0-9-]+$` — The trip's API identity: a lowercase ULID assigned by the server. - **`slug` (required)** `string`, pattern: `^[a-z0-9-]+$` — A readable URL segment, unique on the account. Address trips in the API by \`id\`, not by slug. When a write leaves it out, the server derives one from the title and start date. - **`startDate` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}$` — A whole day, \`2027-03-02\`. Not an instant: it has no time and no zone. - **`status` (required)** `string`, possible values: `"booked", "planning"` - **`summary` (required)** `string | null`, maxLength: `500` - **`title` (required)** `string`, minLength: `1`, maxLength: `200` - **`travellers` (required)** `integer`, minimum: `1` — How many people are on the trip. At least one. - **`updatedAt` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?\+00:00$` — An instant in UTC: ISO 8601 ending \`+00:00\`, never \`Z\` and never bare. - **`version` (required)** `integer`, minimum: `1` — Goes up by one on every change. The \`ETag\` header carries it; send that back as \`If-Match\` to change the trip. **Example:** ```json { "id": "", "slug": "", "version": 1, "title": "", "status": "booked", "startDate": "", "endDate": "", "summary": null, "travellers": 1, "countries": [ "" ], "createdAt": "", "updatedAt": "" } ``` ##### Status: 400 A malformed body (\`invalid\_body\`, \`unknown\_field\`) or an \`If-Match\` that is not a version, including \`\*\` (\`invalid\_if\_match\`). ###### Content-Type: application/json - **`error` (required)** `object` - **`code` (required)** `string`, possible values: `"unauthorized", "not_found", "invalid_body", "unknown_field", "validation_failed", "slug_conflict", "version_conflict", "precondition_required", "invalid_if_match", "forbidden", "insufficient_scope", "cross_site_request", "payload_too_large", "unsupported_media_type", "rate_limited", "invalid_query"` - **`message` (required)** `string` — Written for a person, and names the field it is about. Branch on \`code\`, not on this: its wording may change. **Example:** ```json { "error": { "code": "unauthorized", "message": "" } } ``` ##### Status: 401 No valid credential, or a credential for a user who no longer has an active Nookivia account. Every trip route requires one. ###### Content-Type: application/json - **`error` (required)** `object` - **`code` (required)** `string`, possible values: `"unauthorized", "not_found", "invalid_body", "unknown_field", "validation_failed", "slug_conflict", "version_conflict", "precondition_required", "invalid_if_match", "forbidden", "insufficient_scope", "cross_site_request", "payload_too_large", "unsupported_media_type", "rate_limited", "invalid_query"` - **`message` (required)** `string` — Written for a person, and names the field it is about. Branch on \`code\`, not on this: its wording may change. **Example:** ```json { "error": { "code": "unauthorized", "message": "" } } ``` ##### Status: 403 The credential is valid but may not do this: it was not granted the scope the route needs (\`insufficient\_scope\`, with an RFC 6750 \`WWW-Authenticate\` challenge naming it), or it does not act for a user (\`forbidden\`). **Headers:** - **`WWW-Authenticate`** On `insufficient_scope`: `Bearer error="insufficient_scope", scope="…"`. `string` ###### Content-Type: application/json - **`error` (required)** `object` - **`code` (required)** `string`, possible values: `"unauthorized", "not_found", "invalid_body", "unknown_field", "validation_failed", "slug_conflict", "version_conflict", "precondition_required", "invalid_if_match", "forbidden", "insufficient_scope", "cross_site_request", "payload_too_large", "unsupported_media_type", "rate_limited", "invalid_query"` - **`message` (required)** `string` — Written for a person, and names the field it is about. Branch on \`code\`, not on this: its wording may change. **Example:** ```json { "error": { "code": "unauthorized", "message": "" } } ``` ##### Status: 404 No trip with that id on the authenticated user's account. ###### Content-Type: application/json - **`error` (required)** `object` - **`code` (required)** `string`, possible values: `"unauthorized", "not_found", "invalid_body", "unknown_field", "validation_failed", "slug_conflict", "version_conflict", "precondition_required", "invalid_if_match", "forbidden", "insufficient_scope", "cross_site_request", "payload_too_large", "unsupported_media_type", "rate_limited", "invalid_query"` - **`message` (required)** `string` — Written for a person, and names the field it is about. Branch on \`code\`, not on this: its wording may change. **Example:** ```json { "error": { "code": "unauthorized", "message": "" } } ``` ##### Status: 409 The trip changed since you last read it (\`version\_conflict\`, which carries the trip as it now stands so you can show what changed), or the slug you chose is taken (\`slug\_conflict\`). **Headers:** - **`ETag`** The trip's version, weak-validated. Send it as `If-Match` to write. `string`, pattern: `^W/"\d+"$` ###### Content-Type: application/json \`trip\` is present when \`error.code\` is \`version\_conflict\`, carrying the trip as it now stands, and absent when it is \`slug\_conflict\`. - **`error` (required)** `object` - **`code` (required)** `string`, possible values: `"unauthorized", "not_found", "invalid_body", "unknown_field", "validation_failed", "slug_conflict", "version_conflict", "precondition_required", "invalid_if_match", "forbidden", "insufficient_scope", "cross_site_request", "payload_too_large", "unsupported_media_type", "rate_limited", "invalid_query"` - **`message` (required)** `string` — Written for a person, and names the field it is about. Branch on \`code\`, not on this: its wording may change. - **`trip`** `object` — A trip on the authenticated user's account. - **`countries` (required)** `array` **Items:** `string`, pattern: `^[A-Z]{2}$` — A two-letter uppercase ISO country code. - **`createdAt` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?\+00:00$` — An instant in UTC: ISO 8601 ending \`+00:00\`, never \`Z\` and never bare. - **`endDate` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}$` — A whole day, \`2027-03-02\`. Not an instant: it has no time and no zone. - **`id` (required)** `string`, pattern: `^[a-z0-9-]+$` — The trip's API identity: a lowercase ULID assigned by the server. - **`slug` (required)** `string`, pattern: `^[a-z0-9-]+$` — A readable URL segment, unique on the account. Address trips in the API by \`id\`, not by slug. When a write leaves it out, the server derives one from the title and start date. - **`startDate` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}$` — A whole day, \`2027-03-02\`. Not an instant: it has no time and no zone. - **`status` (required)** `string`, possible values: `"booked", "planning"` - **`summary` (required)** `string | null`, maxLength: `500` - **`title` (required)** `string`, minLength: `1`, maxLength: `200` - **`travellers` (required)** `integer`, minimum: `1` — How many people are on the trip. At least one. - **`updatedAt` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?\+00:00$` — An instant in UTC: ISO 8601 ending \`+00:00\`, never \`Z\` and never bare. - **`version` (required)** `integer`, minimum: `1` — Goes up by one on every change. The \`ETag\` header carries it; send that back as \`If-Match\` to change the trip. **Example:** ```json { "error": { "code": "unauthorized", "message": "" }, "trip": { "id": "", "slug": "", "version": 1, "title": "", "status": "booked", "startDate": "", "endDate": "", "summary": null, "travellers": 1, "countries": [ "" ], "createdAt": "", "updatedAt": "" } } ``` ##### Status: 413 The body is larger than 16384 bytes (\`payload\_too\_large\`). ###### Content-Type: application/json - **`error` (required)** `object` - **`code` (required)** `string`, possible values: `"unauthorized", "not_found", "invalid_body", "unknown_field", "validation_failed", "slug_conflict", "version_conflict", "precondition_required", "invalid_if_match", "forbidden", "insufficient_scope", "cross_site_request", "payload_too_large", "unsupported_media_type", "rate_limited", "invalid_query"` - **`message` (required)** `string` — Written for a person, and names the field it is about. Branch on \`code\`, not on this: its wording may change. **Example:** ```json { "error": { "code": "unauthorized", "message": "" } } ``` ##### Status: 415 The body is not declared as JSON (\`unsupported\_media\_type\`). Send \`Content-Type: application/json\`. ###### Content-Type: application/json - **`error` (required)** `object` - **`code` (required)** `string`, possible values: `"unauthorized", "not_found", "invalid_body", "unknown_field", "validation_failed", "slug_conflict", "version_conflict", "precondition_required", "invalid_if_match", "forbidden", "insufficient_scope", "cross_site_request", "payload_too_large", "unsupported_media_type", "rate_limited", "invalid_query"` - **`message` (required)** `string` — Written for a person, and names the field it is about. Branch on \`code\`, not on this: its wording may change. **Example:** ```json { "error": { "code": "unauthorized", "message": "" } } ``` ##### Status: 422 A field broke one of its rules. \`message\` says which rule, and names the field. ###### Content-Type: application/json - **`error` (required)** `object` - **`code` (required)** `string`, possible values: `"unauthorized", "not_found", "invalid_body", "unknown_field", "validation_failed", "slug_conflict", "version_conflict", "precondition_required", "invalid_if_match", "forbidden", "insufficient_scope", "cross_site_request", "payload_too_large", "unsupported_media_type", "rate_limited", "invalid_query"` - **`message` (required)** `string` — Written for a person, and names the field it is about. Branch on \`code\`, not on this: its wording may change. **Example:** ```json { "error": { "code": "unauthorized", "message": "" } } ``` ##### Status: 428 No \`If-Match\`. Send the version you last read. ###### Content-Type: application/json - **`error` (required)** `object` - **`code` (required)** `string`, possible values: `"unauthorized", "not_found", "invalid_body", "unknown_field", "validation_failed", "slug_conflict", "version_conflict", "precondition_required", "invalid_if_match", "forbidden", "insufficient_scope", "cross_site_request", "payload_too_large", "unsupported_media_type", "rate_limited", "invalid_query"` - **`message` (required)** `string` — Written for a person, and names the field it is about. Branch on \`code\`, not on this: its wording may change. **Example:** ```json { "error": { "code": "unauthorized", "message": "" } } ``` ##### Status: 429 Too many requests, from this address or on this credential. Limits are per minute; wait the number of seconds in \`Retry-After\`. **Headers:** - **`Retry-After`** Seconds to wait before trying again. `integer`, minimum: `1` ###### Content-Type: application/json - **`error` (required)** `object` - **`code` (required)** `string`, possible values: `"unauthorized", "not_found", "invalid_body", "unknown_field", "validation_failed", "slug_conflict", "version_conflict", "precondition_required", "invalid_if_match", "forbidden", "insufficient_scope", "cross_site_request", "payload_too_large", "unsupported_media_type", "rate_limited", "invalid_query"` - **`message` (required)** `string` — Written for a person, and names the field it is about. Branch on \`code\`, not on this: its wording may change. **Example:** ```json { "error": { "code": "unauthorized", "message": "" } } ``` ### Delete a trip - **Method:** `DELETE` - **Path:** `/api/v1/trips/{id}` - **Operation ID:** `deleteApiV1TripsById` - **Tags:** Trips #### Authentication - **apiKey** Scopes: trips:write ``` { "type": "http", "scheme": "bearer", "description": "An API key (`ak_...`), sent as `Authorization: Bearer `. It acts as the user who created it, and holds only the scopes it was created with." } ``` An API key (`ak_...`), sent as `Authorization: Bearer `. It acts as the user who created it, and holds only the scopes it was created with. Or: - **oauthToken** Scopes: trips:write ``` { "type": "http", "scheme": "bearer", "description": "An OAuth 2.0 access token an app obtained with the user’s consent, sent as `Authorization: Bearer `. It holds only the scopes the user granted. The OpenAPI document at /api/openapi.json describes the authorization code flow, with PKCE, and its endpoints." } ``` An OAuth 2.0 access token an app obtained with the user’s consent, sent as `Authorization: Bearer `. It holds only the scopes the user granted. The OpenAPI document at /api/openapi.json describes the authorization code flow, with PKCE, and its endpoints. #### Parameters ##### `id` required - **In:** `path` The trip id. A value that could not be a trip id answers 404. `string`, pattern: `^[a-z0-9-]+$` ##### `If-Match` required - **In:** `header` The trip's `ETag` from your last read. Required: without it the request answers 428. `*` is refused with 400, because a change always names the version it expects. `string` #### Responses ##### Status: 204 Deleted. ##### Status: 400 An \`If-Match\` that is not a version, including \`\*\`. ###### Content-Type: application/json - **`error` (required)** `object` - **`code` (required)** `string`, possible values: `"unauthorized", "not_found", "invalid_body", "unknown_field", "validation_failed", "slug_conflict", "version_conflict", "precondition_required", "invalid_if_match", "forbidden", "insufficient_scope", "cross_site_request", "payload_too_large", "unsupported_media_type", "rate_limited", "invalid_query"` - **`message` (required)** `string` — Written for a person, and names the field it is about. Branch on \`code\`, not on this: its wording may change. **Example:** ```json { "error": { "code": "unauthorized", "message": "" } } ``` ##### Status: 401 No valid credential, or a credential for a user who no longer has an active Nookivia account. Every trip route requires one. ###### Content-Type: application/json - **`error` (required)** `object` - **`code` (required)** `string`, possible values: `"unauthorized", "not_found", "invalid_body", "unknown_field", "validation_failed", "slug_conflict", "version_conflict", "precondition_required", "invalid_if_match", "forbidden", "insufficient_scope", "cross_site_request", "payload_too_large", "unsupported_media_type", "rate_limited", "invalid_query"` - **`message` (required)** `string` — Written for a person, and names the field it is about. Branch on \`code\`, not on this: its wording may change. **Example:** ```json { "error": { "code": "unauthorized", "message": "" } } ``` ##### Status: 403 The credential is valid but may not do this: it was not granted the scope the route needs (\`insufficient\_scope\`, with an RFC 6750 \`WWW-Authenticate\` challenge naming it), or it does not act for a user (\`forbidden\`). **Headers:** - **`WWW-Authenticate`** On `insufficient_scope`: `Bearer error="insufficient_scope", scope="…"`. `string` ###### Content-Type: application/json - **`error` (required)** `object` - **`code` (required)** `string`, possible values: `"unauthorized", "not_found", "invalid_body", "unknown_field", "validation_failed", "slug_conflict", "version_conflict", "precondition_required", "invalid_if_match", "forbidden", "insufficient_scope", "cross_site_request", "payload_too_large", "unsupported_media_type", "rate_limited", "invalid_query"` - **`message` (required)** `string` — Written for a person, and names the field it is about. Branch on \`code\`, not on this: its wording may change. **Example:** ```json { "error": { "code": "unauthorized", "message": "" } } ``` ##### Status: 404 No trip with that id on the authenticated user's account. ###### Content-Type: application/json - **`error` (required)** `object` - **`code` (required)** `string`, possible values: `"unauthorized", "not_found", "invalid_body", "unknown_field", "validation_failed", "slug_conflict", "version_conflict", "precondition_required", "invalid_if_match", "forbidden", "insufficient_scope", "cross_site_request", "payload_too_large", "unsupported_media_type", "rate_limited", "invalid_query"` - **`message` (required)** `string` — Written for a person, and names the field it is about. Branch on \`code\`, not on this: its wording may change. **Example:** ```json { "error": { "code": "unauthorized", "message": "" } } ``` ##### Status: 409 The trip changed since you last read it. The body carries it as it now stands. **Headers:** - **`ETag`** The trip's version, weak-validated. Send it as `If-Match` to write. `string`, pattern: `^W/"\d+"$` ###### Content-Type: application/json - **`error` (required)** `object` - **`code` (required)** `string`, possible values: `"unauthorized", "not_found", "invalid_body", "unknown_field", "validation_failed", "slug_conflict", "version_conflict", "precondition_required", "invalid_if_match", "forbidden", "insufficient_scope", "cross_site_request", "payload_too_large", "unsupported_media_type", "rate_limited", "invalid_query"` - **`message` (required)** `string` — Written for a person, and names the field it is about. Branch on \`code\`, not on this: its wording may change. - **`trip` (required)** `object` — A trip on the authenticated user's account. - **`countries` (required)** `array` **Items:** `string`, pattern: `^[A-Z]{2}$` — A two-letter uppercase ISO country code. - **`createdAt` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?\+00:00$` — An instant in UTC: ISO 8601 ending \`+00:00\`, never \`Z\` and never bare. - **`endDate` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}$` — A whole day, \`2027-03-02\`. Not an instant: it has no time and no zone. - **`id` (required)** `string`, pattern: `^[a-z0-9-]+$` — The trip's API identity: a lowercase ULID assigned by the server. - **`slug` (required)** `string`, pattern: `^[a-z0-9-]+$` — A readable URL segment, unique on the account. Address trips in the API by \`id\`, not by slug. When a write leaves it out, the server derives one from the title and start date. - **`startDate` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}$` — A whole day, \`2027-03-02\`. Not an instant: it has no time and no zone. - **`status` (required)** `string`, possible values: `"booked", "planning"` - **`summary` (required)** `string | null`, maxLength: `500` - **`title` (required)** `string`, minLength: `1`, maxLength: `200` - **`travellers` (required)** `integer`, minimum: `1` — How many people are on the trip. At least one. - **`updatedAt` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?\+00:00$` — An instant in UTC: ISO 8601 ending \`+00:00\`, never \`Z\` and never bare. - **`version` (required)** `integer`, minimum: `1` — Goes up by one on every change. The \`ETag\` header carries it; send that back as \`If-Match\` to change the trip. **Example:** ```json { "error": { "code": "unauthorized", "message": "" }, "trip": { "id": "", "slug": "", "version": 1, "title": "", "status": "booked", "startDate": "", "endDate": "", "summary": null, "travellers": 1, "countries": [ "" ], "createdAt": "", "updatedAt": "" } } ``` ##### Status: 428 No \`If-Match\`. Send the version you last read. ###### Content-Type: application/json - **`error` (required)** `object` - **`code` (required)** `string`, possible values: `"unauthorized", "not_found", "invalid_body", "unknown_field", "validation_failed", "slug_conflict", "version_conflict", "precondition_required", "invalid_if_match", "forbidden", "insufficient_scope", "cross_site_request", "payload_too_large", "unsupported_media_type", "rate_limited", "invalid_query"` - **`message` (required)** `string` — Written for a person, and names the field it is about. Branch on \`code\`, not on this: its wording may change. **Example:** ```json { "error": { "code": "unauthorized", "message": "" } } ``` ##### Status: 429 Too many requests, from this address or on this credential. Limits are per minute; wait the number of seconds in \`Retry-After\`. **Headers:** - **`Retry-After`** Seconds to wait before trying again. `integer`, minimum: `1` ###### Content-Type: application/json - **`error` (required)** `object` - **`code` (required)** `string`, possible values: `"unauthorized", "not_found", "invalid_body", "unknown_field", "validation_failed", "slug_conflict", "version_conflict", "precondition_required", "invalid_if_match", "forbidden", "insufficient_scope", "cross_site_request", "payload_too_large", "unsupported_media_type", "rate_limited", "invalid_query"` - **`message` (required)** `string` — Written for a person, and names the field it is about. Branch on \`code\`, not on this: its wording may change. **Example:** ```json { "error": { "code": "unauthorized", "message": "" } } ``` ## Schemas ### TripList - **Type:** `object` * **`nextCursor` (required)** `string | null` — Pass as \`cursor\` to get the next page; \`null\` on the last. Opaque -- do not build or parse one. * **`trips` (required)** `array` **Items:** A trip on the authenticated user's account. - **`countries` (required)** `array` **Items:** `string`, pattern: `^[A-Z]{2}$` — A two-letter uppercase ISO country code. - **`createdAt` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?\+00:00$` — An instant in UTC: ISO 8601 ending \`+00:00\`, never \`Z\` and never bare. - **`endDate` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}$` — A whole day, \`2027-03-02\`. Not an instant: it has no time and no zone. - **`id` (required)** `string`, pattern: `^[a-z0-9-]+$` — The trip's API identity: a lowercase ULID assigned by the server. - **`slug` (required)** `string`, pattern: `^[a-z0-9-]+$` — A readable URL segment, unique on the account. Address trips in the API by \`id\`, not by slug. When a write leaves it out, the server derives one from the title and start date. - **`startDate` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}$` — A whole day, \`2027-03-02\`. Not an instant: it has no time and no zone. - **`status` (required)** `string`, possible values: `"booked", "planning"` - **`summary` (required)** `string | null`, maxLength: `500` - **`title` (required)** `string`, minLength: `1`, maxLength: `200` - **`travellers` (required)** `integer`, minimum: `1` — How many people are on the trip. At least one. - **`updatedAt` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?\+00:00$` — An instant in UTC: ISO 8601 ending \`+00:00\`, never \`Z\` and never bare. - **`version` (required)** `integer`, minimum: `1` — Goes up by one on every change. The \`ETag\` header carries it; send that back as \`If-Match\` to change the trip. **Example:** ```json { "trips": [ { "id": "", "slug": "", "version": 1, "title": "", "status": "booked", "startDate": "", "endDate": "", "summary": null, "travellers": 1, "countries": [ "" ], "createdAt": "", "updatedAt": "" } ], "nextCursor": null } ``` ### Trip - **Type:** `object` A trip on the authenticated user's account. - **`countries` (required)** `array` **Items:** `string`, pattern: `^[A-Z]{2}$` — A two-letter uppercase ISO country code. - **`createdAt` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?\+00:00$` — An instant in UTC: ISO 8601 ending \`+00:00\`, never \`Z\` and never bare. - **`endDate` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}$` — A whole day, \`2027-03-02\`. Not an instant: it has no time and no zone. - **`id` (required)** `string`, pattern: `^[a-z0-9-]+$` — The trip's API identity: a lowercase ULID assigned by the server. - **`slug` (required)** `string`, pattern: `^[a-z0-9-]+$` — A readable URL segment, unique on the account. Address trips in the API by \`id\`, not by slug. When a write leaves it out, the server derives one from the title and start date. - **`startDate` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}$` — A whole day, \`2027-03-02\`. Not an instant: it has no time and no zone. - **`status` (required)** `string`, possible values: `"booked", "planning"` - **`summary` (required)** `string | null`, maxLength: `500` - **`title` (required)** `string`, minLength: `1`, maxLength: `200` - **`travellers` (required)** `integer`, minimum: `1` — How many people are on the trip. At least one. - **`updatedAt` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?\+00:00$` — An instant in UTC: ISO 8601 ending \`+00:00\`, never \`Z\` and never bare. - **`version` (required)** `integer`, minimum: `1` — Goes up by one on every change. The \`ETag\` header carries it; send that back as \`If-Match\` to change the trip. **Example:** ```json { "id": "", "slug": "", "version": 1, "title": "", "status": "booked", "startDate": "", "endDate": "", "summary": null, "travellers": 1, "countries": [ "" ], "createdAt": "", "updatedAt": "" } ``` ### ApiError - **Type:** `object` * **`error` (required)** `object` - **`code` (required)** `string`, possible values: `"unauthorized", "not_found", "invalid_body", "unknown_field", "validation_failed", "slug_conflict", "version_conflict", "precondition_required", "invalid_if_match", "forbidden", "insufficient_scope", "cross_site_request", "payload_too_large", "unsupported_media_type", "rate_limited", "invalid_query"` - **`message` (required)** `string` — Written for a person, and names the field it is about. Branch on \`code\`, not on this: its wording may change. **Example:** ```json { "error": { "code": "unauthorized", "message": "" } } ``` ### TripInput - **Type:** `object` * **`endDate` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}$` — A whole day, \`2027-03-02\`. Not an instant: it has no time and no zone. * **`startDate` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}$` — A whole day, \`2027-03-02\`. Not an instant: it has no time and no zone. * **`status` (required)** `string`, possible values: `"booked", "planning"` * **`title` (required)** `string`, minLength: `1`, maxLength: `200` * **`countries`** `array` — Every country the trip visits, in the order the itinerary meets them. **Items:** `string`, pattern: `^[A-Z]{2}$` — A two-letter uppercase ISO country code. - Min items: `1` * **`slug`** `string`, pattern: `^[a-z0-9-]+$` — A readable URL segment, unique on the account. Address trips in the API by \`id\`, not by slug. When a write leaves it out, the server derives one from the title and start date. * **`summary`** `string | null`, maxLength: `500` * **`travellers`** `integer`, minimum: `1` — How many people are on the trip. At least one. **Additional properties:** never (false schema) **Example:** ```json { "title": "", "status": "booked", "startDate": "", "endDate": "", "summary": null, "travellers": 1, "countries": [ "" ], "slug": "" } ``` ### WriteConflict - **Type:** `object` `trip` is present when `error.code` is `version_conflict`, carrying the trip as it now stands, and absent when it is `slug_conflict`. - **`error` (required)** `object` - **`code` (required)** `string`, possible values: `"unauthorized", "not_found", "invalid_body", "unknown_field", "validation_failed", "slug_conflict", "version_conflict", "precondition_required", "invalid_if_match", "forbidden", "insufficient_scope", "cross_site_request", "payload_too_large", "unsupported_media_type", "rate_limited", "invalid_query"` - **`message` (required)** `string` — Written for a person, and names the field it is about. Branch on \`code\`, not on this: its wording may change. - **`trip`** `object` — A trip on the authenticated user's account. - **`countries` (required)** `array` **Items:** `string`, pattern: `^[A-Z]{2}$` — A two-letter uppercase ISO country code. - **`createdAt` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?\+00:00$` — An instant in UTC: ISO 8601 ending \`+00:00\`, never \`Z\` and never bare. - **`endDate` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}$` — A whole day, \`2027-03-02\`. Not an instant: it has no time and no zone. - **`id` (required)** `string`, pattern: `^[a-z0-9-]+$` — The trip's API identity: a lowercase ULID assigned by the server. - **`slug` (required)** `string`, pattern: `^[a-z0-9-]+$` — A readable URL segment, unique on the account. Address trips in the API by \`id\`, not by slug. When a write leaves it out, the server derives one from the title and start date. - **`startDate` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}$` — A whole day, \`2027-03-02\`. Not an instant: it has no time and no zone. - **`status` (required)** `string`, possible values: `"booked", "planning"` - **`summary` (required)** `string | null`, maxLength: `500` - **`title` (required)** `string`, minLength: `1`, maxLength: `200` - **`travellers` (required)** `integer`, minimum: `1` — How many people are on the trip. At least one. - **`updatedAt` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?\+00:00$` — An instant in UTC: ISO 8601 ending \`+00:00\`, never \`Z\` and never bare. - **`version` (required)** `integer`, minimum: `1` — Goes up by one on every change. The \`ETag\` header carries it; send that back as \`If-Match\` to change the trip. **Example:** ```json { "error": { "code": "unauthorized", "message": "" }, "trip": { "id": "", "slug": "", "version": 1, "title": "", "status": "booked", "startDate": "", "endDate": "", "summary": null, "travellers": 1, "countries": [ "" ], "createdAt": "", "updatedAt": "" } } ``` ### VersionConflict - **Type:** `object` * **`error` (required)** `object` - **`code` (required)** `string`, possible values: `"unauthorized", "not_found", "invalid_body", "unknown_field", "validation_failed", "slug_conflict", "version_conflict", "precondition_required", "invalid_if_match", "forbidden", "insufficient_scope", "cross_site_request", "payload_too_large", "unsupported_media_type", "rate_limited", "invalid_query"` - **`message` (required)** `string` — Written for a person, and names the field it is about. Branch on \`code\`, not on this: its wording may change. * **`trip` (required)** `object` — A trip on the authenticated user's account. - **`countries` (required)** `array` **Items:** `string`, pattern: `^[A-Z]{2}$` — A two-letter uppercase ISO country code. - **`createdAt` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?\+00:00$` — An instant in UTC: ISO 8601 ending \`+00:00\`, never \`Z\` and never bare. - **`endDate` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}$` — A whole day, \`2027-03-02\`. Not an instant: it has no time and no zone. - **`id` (required)** `string`, pattern: `^[a-z0-9-]+$` — The trip's API identity: a lowercase ULID assigned by the server. - **`slug` (required)** `string`, pattern: `^[a-z0-9-]+$` — A readable URL segment, unique on the account. Address trips in the API by \`id\`, not by slug. When a write leaves it out, the server derives one from the title and start date. - **`startDate` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}$` — A whole day, \`2027-03-02\`. Not an instant: it has no time and no zone. - **`status` (required)** `string`, possible values: `"booked", "planning"` - **`summary` (required)** `string | null`, maxLength: `500` - **`title` (required)** `string`, minLength: `1`, maxLength: `200` - **`travellers` (required)** `integer`, minimum: `1` — How many people are on the trip. At least one. - **`updatedAt` (required)** `string`, pattern: `^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?\+00:00$` — An instant in UTC: ISO 8601 ending \`+00:00\`, never \`Z\` and never bare. - **`version` (required)** `integer`, minimum: `1` — Goes up by one on every change. The \`ETag\` header carries it; send that back as \`If-Match\` to change the trip. **Example:** ```json { "error": { "code": "unauthorized", "message": "" }, "trip": { "id": "", "slug": "", "version": 1, "title": "", "status": "booked", "startDate": "", "endDate": "", "summary": null, "travellers": 1, "countries": [ "" ], "createdAt": "", "updatedAt": "" } } ```