{"openapi":"3.1.0","info":{"title":"Lotse server API","version":"1.0.0","summary":"Links, campaigns, pushes to one user, server events and user deletion for the backend of an app, and signed webhooks back.","description":"The server API lets the backend of an app work with its Lotse project: create\ndeep links, send push campaigns and pushes to one user, report events of its\nusers and delete users. Lotse reports back with signed webhooks, described\nunder `webhooks`. REST with JSON in UTF-8 under `/v1/`.\n\n## Authentication\n\nEvery call but `GET /v1/openapi.json` carries a server key:\n`Authorization: Bearer lt_srv_…`. The owner of a project creates it in the\nLotse console under Developers → Server keys, with the scopes it needs, which\nstay fixed. The project comes from the key and only from there: no path,\nquery or body names a project. Each operation names its scope in its\ndescription, in `security` and in `x-lotse-scope`. A key without it gets\n`403 forbidden`.\n\nRequests with an `Origin` header, which browsers send, get `403 forbidden`:\nkeep the key in your backend. Only this document may be loaded from a\nbrowser.\n\n## Errors\n\nEvery answer that is not 2xx has the body `{\"error\": \"<code>\", \"message\":\n\"<sentence>\"}`, the sentence in English, never a stack trace, and never\nrepeating a value you sent.\n\n| Status | `error` | When |\n|---|---|---|\n| 401 | `unauthorized` | No server key, or one unknown, revoked or expired. |\n| 403 | `forbidden` | The key lacks the scope of the operation (the message names it), or the request came from a browser (`Origin` header). |\n| 403 | `project_suspended` | The project is suspended: it can be read and cleaned up, nothing created or sent. |\n| 402 | `plan_limited` | The project stayed above the monthly active devices of its plan for 30 days and is limited: no new campaigns, sends or messages until it has a larger plan. Links, events, reading and deleting go on, and so do sends already running. |\n| 400 | `invalid_input` | Input out of its limits, an unknown field, a body that is no JSON object in UTF-8, a malformed `limit`, `cursor` or `Idempotency-Key`. |\n| 404 | `not_found` | No such path, or no such resource in the project of the key. A resource of another project is not found either. |\n| 405 | `method_not_allowed` | The path exists, the method does not; `Allow` names the methods. |\n| 409 | `conflict` | The state forbids it (a taken slug, a link still in use), or the `Idempotency-Key` belongs to another request, to one still running (with `Retry-After`) or to one whose outcome is unknown. |\n| 413 | `invalid_input` | The body is larger than 64 KiB, for `POST /v1/events` 256 KiB. |\n| 422 | `identity_verification_required` | A message to a `userId` that has no install verified as that user. |\n| 429 | `quota_exceeded` | A quota of the project, the same as in the console. No `Retry-After`: waiting does not help, deleting does. |\n| 429 | `rate_limited` | A budget of calls or of an operation, with `Retry-After` in seconds. |\n| 500 | `internal_error` | A failure of the server, without detail. |\n| 503 | `unavailable` | Work the server could not finish now, with `Retry-After`: sending the same request again does what is missing and nothing twice. |\n\nThe checks run in this order: browser (`403`), key (`401`), budget of\ncalls (`429`), path and method (`404`, `405`), scope (`403`), suspension\n(`403`), body (`413`, `400`), `Idempotency-Key`, budget of writes (`429`),\nthen the operation with its input, quotas and budgets.\n\n## Limits\n\n- Each key: 600 calls an hour, of which\n  60 writing (`POST`, `DELETE`). All keys of a\n  project together: 6000 calls an hour. Hours\n  are fixed windows; over a budget the answer is `429 rate_limited` with\n  `Retry-After`, the seconds to the next window.\n- Bodies: at most 64 KiB, for `POST /v1/events`\n  256 KiB. Above, `413`.\n- Quotas of the project, e.g. 5000 links on Free\n  (50000 on Pro) or 20 sent\n  campaigns in 24 hours, shared with the console: `429 quota_exceeded`.\n\n## Idempotency\n\nEvery `POST` takes an optional `Idempotency-Key` header, kept\n24 hours per server key. A retry of the same\nrequest (same operation, path and body) gets the stored answer again with\n`Idempotent-Replayed: true`, without running twice. The same key with another\nrequest gets `409 conflict`.\n\nA `429` or `503` frees the key: the retry with it runs again. A `503` may come\nafter the work was committed; the retry does what is missing and nothing\ntwice. After a `500` or a timeout, a retry with the same key never runs a\nsecond time. It gets the stored answer if the first request finished,\notherwise `409 conflict`: with `Retry-After` while the first request may\nstill run (up to 5 minutes), then saying\nits outcome is unknown. Then check the resource with `GET`, and send the\nrequest with a new key if it is missing.\n\n## Lists\n\nLists answer `{\"data\": [...], \"nextCursor\": \"<id>\" | null}`, newest first.\nPass `nextCursor` as `cursor` for the next page; `null` ends the list.\n\n## Answers\n\nEvery answer carries `Cache-Control: no-store`. Times are ISO 8601 in UTC;\ntimes you send need `Z` or an offset. Fields without a value are left out\nunless their schema allows `null`.\n\n## Webhooks\n\nHttps endpoints per project, 5 on Free (the plan\nsets the limit), created by the owner in the console under Developers → Webhooks, each with its own\nsigning secret (`whsec_…`) and the events it subscribes to.\nLotse posts `{\"type\", \"timestamp\", \"data\"}` signed according to Standard\nWebhooks (https://www.standardwebhooks.com): verify `webhook-signature`\nwith the secret before you trust a delivery, and reject a\n`webhook-timestamp` more than 5\nminutes off your clock.\nDelivery is at least once and in no particular order: deduplicate on\n`webhook-id`. Answer with 2xx within 5\nseconds; anything else, a redirect included, is retried, up to\n8 attempts within about 24 hours. An endpoint that fails\nfor 3 days is switched off and its owner gets a\nmail. The console keeps the delivery log 30 days,\nthe payloads 7 days.","contact":{"name":"Lotse","url":"https://lotse.dev"}},"servers":[{"url":"https://api.lotse.dev","description":"Lotse"}],"security":[{"serverKey":[]}],"tags":[{"name":"Links","description":"Deep links on the link domain of the project."},{"name":"Campaigns","description":"Push campaigns to a segment, and the saved segments."},{"name":"Messages","description":"Pushes to one user or one install."},{"name":"Events","description":"Events the backend reports for the users of the app."},{"name":"Users","description":"App users, by the id the app passes to `Lotse.identify`."},{"name":"Document","description":"This document."},{"name":"Webhooks","description":"Events Lotse posts to the endpoints of the project, signed."}],"paths":{"/v1/links":{"get":{"operationId":"listLinks","tags":["Links"],"summary":"List links","description":"The links of the project, newest first.\n\nScope: `links:read`.","x-lotse-scope":"links:read","security":[{"serverKey":["links:read"]}],"parameters":[{"name":"limit","in":"query","required":false,"description":"Entries per page, 100 without it, at most 500: larger values are cut to 500.","schema":{"type":"integer","minimum":1}},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"description":"A page of links.","content":{"application/json":{"schema":{"type":"object","required":["data","nextCursor"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Link"}},"nextCursor":{"type":["string","null"],"description":"Pass as `cursor` for the next page; `null` on the last page."}}}}}},"400":{"$ref":"#/components/responses/InvalidInput"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"createLink","tags":["Links"],"summary":"Create a link","description":"A link under `https://<link domain>/l/<slug>` that opens `targetPath` in the app, or the store of the app and then the route (deferred deep link). Without an app of its platform in the project a device opens the fallback. At most The number of links per project depends on its plan: 5000 on Free, 50000 on Pro.\n\nThe body is at most 64 KiB. A suspended project is refused.\n\nScope: `links:write`.","x-lotse-scope":"links:write","security":[{"serverKey":["links:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkInput"}}}},"x-lotse-max-body-bytes":65536,"responses":{"201":{"description":"The new link.","headers":{"Idempotent-Replayed":{"$ref":"#/components/headers/IdempotentReplayed"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Link"}}}},"400":{"$ref":"#/components/responses/InvalidInput"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/ForbiddenOrSuspended"},"409":{"$ref":"#/components/responses/Conflict"},"413":{"$ref":"#/components/responses/PayloadTooLarge"},"429":{"$ref":"#/components/responses/RateLimitedOrQuotaExceeded"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/links/{id}":{"parameters":[{"name":"id","in":"path","required":true,"description":"Id of the link.","schema":{"type":"integer","minimum":1}}],"get":{"operationId":"getLink","tags":["Links"],"summary":"Get a link","description":"One link of the project.\n\nScope: `links:read`.","x-lotse-scope":"links:read","security":[{"serverKey":["links:read"]}],"responses":{"200":{"description":"The link.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Link"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"delete":{"operationId":"deleteLink","tags":["Links"],"summary":"Delete a link","description":"Deletes the link with its clicks and frees its slug. `409` while something still to go out opens it: a campaign not sent yet, an in-app message or journey variant in draft or active, a push still pending. Installs it brought count as organic afterwards.\n\nScope: `links:write`.","x-lotse-scope":"links:write","security":[{"serverKey":["links:write"]}],"responses":{"204":{"description":"Deleted."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/segments":{"get":{"operationId":"listSegments","tags":["Campaigns"],"summary":"List saved segments","description":"The segments saved in the console, by name, at most 50: always one page. Pass an `id` as `segmentId` when creating a campaign.\n\nScope: `campaigns:read`.","x-lotse-scope":"campaigns:read","security":[{"serverKey":["campaigns:read"]}],"responses":{"200":{"description":"Every saved segment.","content":{"application/json":{"schema":{"type":"object","required":["data","nextCursor"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Segment"}},"nextCursor":{"type":["string","null"],"description":"Pass as `cursor` for the next page; `null` on the last page."}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/campaigns":{"get":{"operationId":"listCampaigns","tags":["Campaigns"],"summary":"List campaigns","description":"The campaigns of the project, newest first, without their reports. Variants of journeys are no campaigns here.\n\nScope: `campaigns:read`.","x-lotse-scope":"campaigns:read","security":[{"serverKey":["campaigns:read"]}],"parameters":[{"name":"limit","in":"query","required":false,"description":"Entries per page, 100 without it, at most 200: larger values are cut to 200.","schema":{"type":"integer","minimum":1}},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"description":"A page of campaigns.","content":{"application/json":{"schema":{"type":"object","required":["data","nextCursor"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Campaign"}},"nextCursor":{"type":["string","null"],"description":"Pass as `cursor` for the next page; `null` on the last page."}}}}}},"400":{"$ref":"#/components/responses/InvalidInput"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"createCampaign","tags":["Campaigns"],"summary":"Create a campaign","description":"A draft. It goes out with `send` or `schedule`. The audience is exactly one of `segmentId`, `segment` and `\"audience\": \"all\"`: without one the call is refused, so no push reaches every device by accident. Drafts per project are limited by plan, 20 on Free.\n\nThe body is at most 64 KiB. A suspended project is refused.\n\nScope: `campaigns:write`.","x-lotse-scope":"campaigns:write","security":[{"serverKey":["campaigns:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampaignInput"}}}},"x-lotse-max-body-bytes":65536,"responses":{"201":{"description":"The new draft.","headers":{"Idempotent-Replayed":{"$ref":"#/components/headers/IdempotentReplayed"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Campaign"}}}},"400":{"$ref":"#/components/responses/InvalidInput"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanLimited"},"403":{"$ref":"#/components/responses/ForbiddenOrSuspended"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"413":{"$ref":"#/components/responses/PayloadTooLarge"},"429":{"$ref":"#/components/responses/RateLimitedOrQuotaExceeded"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/campaigns/{id}":{"parameters":[{"name":"id","in":"path","required":true,"description":"Id of the campaign.","schema":{"type":"integer","minimum":1}}],"get":{"operationId":"getCampaign","tags":["Campaigns"],"summary":"Get a campaign with its report","description":"The campaign and its `report`, as the console shows it. With `goalEvent` the report adds the devices that tracked that event within 24 hours of the push.\n\nScope: `campaigns:read`.","x-lotse-scope":"campaigns:read","security":[{"serverKey":["campaigns:read"]}],"parameters":[{"name":"goalEvent","in":"query","required":false,"description":"Event that counts as reaching the goal.","schema":{"type":"string","minLength":1,"maxLength":100}}],"responses":{"200":{"description":"The campaign.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampaignWithReport"}}}},"400":{"$ref":"#/components/responses/InvalidInput"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/campaigns/{id}/send":{"parameters":[{"name":"id","in":"path","required":true,"description":"Id of the campaign.","schema":{"type":"integer","minimum":1}}],"post":{"operationId":"sendCampaign","tags":["Campaigns"],"summary":"Send a campaign now","description":"Sends a draft or a scheduled campaign now, one push per device of its audience with a valid push token. A campaign already sending comes back as it is, without a second send; a finished one is refused. Sends per project in 24 hours are limited by plan, 20 on Free, shared with the console. `503` when the push worker could not be started: the retry starts it and sends nothing twice.\n\nThe body is at most 64 KiB. A suspended project is refused.\n\nScope: `campaigns:send`.","x-lotse-scope":"campaigns:send","security":[{"serverKey":["campaigns:send"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmptyInput"}}}},"x-lotse-max-body-bytes":65536,"responses":{"200":{"description":"The campaign, sending.","headers":{"Idempotent-Replayed":{"$ref":"#/components/headers/IdempotentReplayed"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Campaign"}}}},"400":{"$ref":"#/components/responses/InvalidInput"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanLimited"},"403":{"$ref":"#/components/responses/ForbiddenOrSuspended"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"413":{"$ref":"#/components/responses/PayloadTooLarge"},"429":{"$ref":"#/components/responses/RateLimitedOrQuotaExceeded"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/Unavailable"}}}},"/v1/campaigns/{id}/schedule":{"parameters":[{"name":"id","in":"path","required":true,"description":"Id of the campaign.","schema":{"type":"integer","minimum":1}}],"post":{"operationId":"scheduleCampaign","tags":["Campaigns"],"summary":"Schedule a campaign","description":"Plans the send of a draft for `sendAt`, or moves a planned one. Suspension, the limitation of the plan and send quota are checked at that time; a campaign that cannot go out then becomes `failed` with `sendError`. At most 30 plans per project an hour (`429 rate_limited`). `503` when the send could not be planned: the campaign is a draft again, and the retry plans it.\n\nThe body is at most 64 KiB. A suspended project is refused.\n\nScope: `campaigns:send`.","x-lotse-scope":"campaigns:send","security":[{"serverKey":["campaigns:send"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleInput"}}}},"x-lotse-max-body-bytes":65536,"responses":{"200":{"description":"The campaign, scheduled.","headers":{"Idempotent-Replayed":{"$ref":"#/components/headers/IdempotentReplayed"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Campaign"}}}},"400":{"$ref":"#/components/responses/InvalidInput"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanLimited"},"403":{"$ref":"#/components/responses/ForbiddenOrSuspended"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"413":{"$ref":"#/components/responses/PayloadTooLarge"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/Unavailable"}}}},"/v1/messages":{"post":{"operationId":"sendMessage","tags":["Messages"],"summary":"Send a push to one user or install","description":"A push to exactly one target. `userId` reaches only installs identified as that user with a valid user hash (identity verification in the console); without one the answer is `422`. `installId` reaches that install, verified or not. No reachable device is no error: `201` with `\"status\": \"no_devices\"`, and the webhook `message.failed`. Limited by plan; on Free at most 10 per user or install an hour and 10000 per project in 24 hours. For many users, send a campaign. With `silent` the push shows nothing and wakes the app instead: no title, body or `targetPath`, but `data` or `badge`. Own `data` keys arrive next to the Lotse keys in `RemoteMessage.data` (FCM) or at the top level of the APNs payload.\n\nThe body is at most 64 KiB. A suspended project is refused.\n\nScope: `messages:send`.","x-lotse-scope":"messages:send","security":[{"serverKey":["messages:send"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageInput"}}}},"x-lotse-max-body-bytes":65536,"responses":{"201":{"description":"The message with its deliveries.","headers":{"Idempotent-Replayed":{"$ref":"#/components/headers/IdempotentReplayed"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Message"}}}},"400":{"$ref":"#/components/responses/InvalidInput"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanLimited"},"403":{"$ref":"#/components/responses/ForbiddenOrSuspended"},"409":{"$ref":"#/components/responses/Conflict"},"413":{"$ref":"#/components/responses/PayloadTooLarge"},"422":{"$ref":"#/components/responses/IdentityVerificationRequired"},"429":{"$ref":"#/components/responses/RateLimitedOrQuotaExceeded"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/messages/{id}":{"parameters":[{"name":"id","in":"path","required":true,"description":"Id of the message.","schema":{"type":"integer","minimum":1}}],"get":{"operationId":"getMessage","tags":["Messages"],"summary":"Get a message","description":"The message with its status, deliveries and opens. Title and body are `null` after 30 days, the message is gone after 90.\n\nScope: `messages:send`.","x-lotse-scope":"messages:send","security":[{"serverKey":["messages:send"]}],"responses":{"200":{"description":"The message.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Message"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/events":{"post":{"operationId":"trackEvents","tags":["Events"],"summary":"Report events","description":"1 to 100 events of your users, e.g. a purchase, all stored or none. Each goes to the install of its user seen last, so segments, journeys and campaign goals count it like one the app sent. A user without an install is created and keeps the event for the event explorer only. An event with an `id` the project has stored before is skipped and counted in `duplicates`. Counts against the event budget of the project, shared with the SDK. `503` when parallel calls wrote the same ids or users: send it again.\n\nThe body is at most 256 KiB. A suspended project is refused.\n\nScope: `events:write`.","x-lotse-scope":"events:write","security":[{"serverKey":["events:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventsInput"}}}},"x-lotse-max-body-bytes":262144,"responses":{"200":{"description":"What was stored.","headers":{"Idempotent-Replayed":{"$ref":"#/components/headers/IdempotentReplayed"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventsAccepted"}}}},"400":{"$ref":"#/components/responses/InvalidInput"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/ForbiddenOrSuspended"},"409":{"$ref":"#/components/responses/Conflict"},"413":{"$ref":"#/components/responses/PayloadTooLarge"},"429":{"$ref":"#/components/responses/RateLimitedOrQuotaExceeded"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/Unavailable"}}}},"/v1/users/{userId}":{"parameters":[{"name":"userId","in":"path","required":true,"description":"The id the app passes to `Lotse.identify`, URL-encoded. The request log of the console keeps the pattern, never the id.","schema":{"type":"string","minLength":1,"maxLength":256}}],"delete":{"operationId":"deleteUser","tags":["Users"],"summary":"Delete a user","description":"Deletes the user with its traits and messages, and the installs identified as the user with their events, feedback, replays and answers. Then the webhook `user.deleted`. Unknown, also after a first delete: `404`.\n\nScope: `users:delete`.","x-lotse-scope":"users:delete","security":[{"serverKey":["users:delete"]}],"responses":{"204":{"description":"Deleted."},"400":{"$ref":"#/components/responses/InvalidInput"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/users/{userId}/entitlements":{"parameters":[{"name":"userId","in":"path","required":true,"description":"The id the app passes to `Lotse.identify`, URL-encoded. The request log of the console keeps the pattern, never the id.","schema":{"type":"string","minLength":1,"maxLength":256}}],"get":{"operationId":"getUserEntitlements","tags":["Users"],"summary":"Read the entitlements of a user","description":"The active entitlements of the user from purchases and grants, the same objects as the webhook `entitlements.changed`. An unknown user has none: `200` with an empty list. The user id counts without identity verification; your backend is trusted.\n\nScope: `purchases:read`.","x-lotse-scope":"purchases:read","security":[{"serverKey":["purchases:read"]}],"responses":{"200":{"description":"The entitlements.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserEntitlements"}}}},"400":{"$ref":"#/components/responses/InvalidInput"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/openapi.json":{"get":{"operationId":"getOpenApi","tags":["Document"],"summary":"This document","description":"The OpenAPI document of the server API. Needs no key and costs no budget, and a browser may load it: it holds no data of any project.","security":[],"responses":{"200":{"description":"The OpenAPI 3.1 document.","headers":{"Cache-Control":{"description":"Public, for 3600 seconds.","schema":{"type":"string"}},"Access-Control-Allow-Origin":{"description":"`*`: any website may read it.","schema":{"type":"string","enum":["*"]}}},"content":{"application/json":{"schema":{"type":"object"}}}}}}}},"webhooks":{"feedback.created":{"post":{"operationId":"webhookFeedbackCreated","tags":["Webhooks"],"summary":"A user sent feedback","description":"Posted to every endpoint that subscribed to `feedback.created`, signed with its secret. Verify `webhook-signature` first.","security":[],"parameters":[{"$ref":"#/components/parameters/WebhookId"},{"$ref":"#/components/parameters/WebhookTimestamp"},{"$ref":"#/components/parameters/WebhookSignature"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WebhookEvent"},{"properties":{"type":{"const":"feedback.created"},"data":{"$ref":"#/components/schemas/FeedbackCreated"}}}]}}}},"responses":{"2XX":{"description":"Received. Any other status, a redirect included, no answer within 5 seconds or no connection is a failed attempt, retried later. At most 4 KiB of the answer are read."}}}},"feedback.replied":{"post":{"operationId":"webhookFeedbackReplied","tags":["Webhooks"],"summary":"A new message in a feedback conversation, by the team or the user","description":"Posted to every endpoint that subscribed to `feedback.replied`, signed with its secret. Verify `webhook-signature` first.","security":[],"parameters":[{"$ref":"#/components/parameters/WebhookId"},{"$ref":"#/components/parameters/WebhookTimestamp"},{"$ref":"#/components/parameters/WebhookSignature"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WebhookEvent"},{"properties":{"type":{"const":"feedback.replied"},"data":{"$ref":"#/components/schemas/FeedbackReplied"}}}]}}}},"responses":{"2XX":{"description":"Received. Any other status, a redirect included, no answer within 5 seconds or no connection is a failed attempt, retried later. At most 4 KiB of the answer are read."}}}},"feature_request.created":{"post":{"operationId":"webhookFeatureRequestCreated","tags":["Webhooks"],"summary":"A new idea on the feature board","description":"Posted to every endpoint that subscribed to `feature_request.created`, signed with its secret. Verify `webhook-signature` first.","security":[],"parameters":[{"$ref":"#/components/parameters/WebhookId"},{"$ref":"#/components/parameters/WebhookTimestamp"},{"$ref":"#/components/parameters/WebhookSignature"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WebhookEvent"},{"properties":{"type":{"const":"feature_request.created"},"data":{"$ref":"#/components/schemas/FeatureRequestCreated"}}}]}}}},"responses":{"2XX":{"description":"Received. Any other status, a redirect included, no answer within 5 seconds or no connection is a failed attempt, retried later. At most 4 KiB of the answer are read."}}}},"survey.answered":{"post":{"operationId":"webhookSurveyAnswered","tags":["Webhooks"],"summary":"A user answered a survey","description":"Posted to every endpoint that subscribed to `survey.answered`, signed with its secret. Verify `webhook-signature` first.","security":[],"parameters":[{"$ref":"#/components/parameters/WebhookId"},{"$ref":"#/components/parameters/WebhookTimestamp"},{"$ref":"#/components/parameters/WebhookSignature"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WebhookEvent"},{"properties":{"type":{"const":"survey.answered"},"data":{"$ref":"#/components/schemas/SurveyAnswered"}}}]}}}},"responses":{"2XX":{"description":"Received. Any other status, a redirect included, no answer within 5 seconds or no connection is a failed attempt, retried later. At most 4 KiB of the answer are read."}}}},"install.attributed":{"post":{"operationId":"webhookInstallAttributed","tags":["Webhooks"],"summary":"An install was credited to a link","description":"Posted to every endpoint that subscribed to `install.attributed`, signed with its secret. Verify `webhook-signature` first.","security":[],"parameters":[{"$ref":"#/components/parameters/WebhookId"},{"$ref":"#/components/parameters/WebhookTimestamp"},{"$ref":"#/components/parameters/WebhookSignature"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WebhookEvent"},{"properties":{"type":{"const":"install.attributed"},"data":{"$ref":"#/components/schemas/InstallAttributed"}}}]}}}},"responses":{"2XX":{"description":"Received. Any other status, a redirect included, no answer within 5 seconds or no connection is a failed attempt, retried later. At most 4 KiB of the answer are read."}}}},"campaign.finished":{"post":{"operationId":"webhookCampaignFinished","tags":["Webhooks"],"summary":"A campaign finished sending","description":"Posted to every endpoint that subscribed to `campaign.finished`, signed with its secret. Verify `webhook-signature` first.","security":[],"parameters":[{"$ref":"#/components/parameters/WebhookId"},{"$ref":"#/components/parameters/WebhookTimestamp"},{"$ref":"#/components/parameters/WebhookSignature"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WebhookEvent"},{"properties":{"type":{"const":"campaign.finished"},"data":{"$ref":"#/components/schemas/CampaignFinished"}}}]}}}},"responses":{"2XX":{"description":"Received. Any other status, a redirect included, no answer within 5 seconds or no connection is a failed attempt, retried later. At most 4 KiB of the answer are read."}}}},"message.delivered":{"post":{"operationId":"webhookMessageDelivered","tags":["Webhooks"],"summary":"A message reached the push provider of at least one device","description":"Posted to every endpoint that subscribed to `message.delivered`, signed with its secret. Verify `webhook-signature` first.","security":[],"parameters":[{"$ref":"#/components/parameters/WebhookId"},{"$ref":"#/components/parameters/WebhookTimestamp"},{"$ref":"#/components/parameters/WebhookSignature"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WebhookEvent"},{"properties":{"type":{"const":"message.delivered"},"data":{"$ref":"#/components/schemas/Message"}}}]}}}},"responses":{"2XX":{"description":"Received. Any other status, a redirect included, no answer within 5 seconds or no connection is a failed attempt, retried later. At most 4 KiB of the answer are read."}}}},"message.failed":{"post":{"operationId":"webhookMessageFailed","tags":["Webhooks"],"summary":"A message reached no device (status failed or no_devices)","description":"Posted to every endpoint that subscribed to `message.failed`, signed with its secret. Verify `webhook-signature` first.","security":[],"parameters":[{"$ref":"#/components/parameters/WebhookId"},{"$ref":"#/components/parameters/WebhookTimestamp"},{"$ref":"#/components/parameters/WebhookSignature"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WebhookEvent"},{"properties":{"type":{"const":"message.failed"},"data":{"$ref":"#/components/schemas/Message"}}}]}}}},"responses":{"2XX":{"description":"Received. Any other status, a redirect included, no answer within 5 seconds or no connection is a failed attempt, retried later. At most 4 KiB of the answer are read."}}}},"user.identified":{"post":{"operationId":"webhookUserIdentified","tags":["Webhooks"],"summary":"An install was identified as a user","description":"Posted to every endpoint that subscribed to `user.identified`, signed with its secret. Verify `webhook-signature` first.","security":[],"parameters":[{"$ref":"#/components/parameters/WebhookId"},{"$ref":"#/components/parameters/WebhookTimestamp"},{"$ref":"#/components/parameters/WebhookSignature"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WebhookEvent"},{"properties":{"type":{"const":"user.identified"},"data":{"$ref":"#/components/schemas/UserIdentified"}}}]}}}},"responses":{"2XX":{"description":"Received. Any other status, a redirect included, no answer within 5 seconds or no connection is a failed attempt, retried later. At most 4 KiB of the answer are read."}}}},"user.deleted":{"post":{"operationId":"webhookUserDeleted","tags":["Webhooks"],"summary":"A user was deleted","description":"Posted to every endpoint that subscribed to `user.deleted`, signed with its secret. Verify `webhook-signature` first.","security":[],"parameters":[{"$ref":"#/components/parameters/WebhookId"},{"$ref":"#/components/parameters/WebhookTimestamp"},{"$ref":"#/components/parameters/WebhookSignature"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WebhookEvent"},{"properties":{"type":{"const":"user.deleted"},"data":{"$ref":"#/components/schemas/UserDeleted"}}}]}}}},"responses":{"2XX":{"description":"Received. Any other status, a redirect included, no answer within 5 seconds or no connection is a failed attempt, retried later. At most 4 KiB of the answer are read."}}}},"subscription.started":{"post":{"operationId":"webhookSubscriptionStarted","tags":["Webhooks"],"summary":"A subscription or its trial started","description":"Posted to every endpoint that subscribed to `subscription.started`, signed with its secret. Verify `webhook-signature` first.","security":[],"parameters":[{"$ref":"#/components/parameters/WebhookId"},{"$ref":"#/components/parameters/WebhookTimestamp"},{"$ref":"#/components/parameters/WebhookSignature"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WebhookEvent"},{"properties":{"type":{"const":"subscription.started"},"data":{"$ref":"#/components/schemas/PurchaseEvent"}}}]}}}},"responses":{"2XX":{"description":"Received. Any other status, a redirect included, no answer within 5 seconds or no connection is a failed attempt, retried later. At most 4 KiB of the answer are read."}}}},"subscription.renewed":{"post":{"operationId":"webhookSubscriptionRenewed","tags":["Webhooks"],"summary":"A subscription renewed","description":"Posted to every endpoint that subscribed to `subscription.renewed`, signed with its secret. Verify `webhook-signature` first.","security":[],"parameters":[{"$ref":"#/components/parameters/WebhookId"},{"$ref":"#/components/parameters/WebhookTimestamp"},{"$ref":"#/components/parameters/WebhookSignature"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WebhookEvent"},{"properties":{"type":{"const":"subscription.renewed"},"data":{"$ref":"#/components/schemas/PurchaseEvent"}}}]}}}},"responses":{"2XX":{"description":"Received. Any other status, a redirect included, no answer within 5 seconds or no connection is a failed attempt, retried later. At most 4 KiB of the answer are read."}}}},"subscription.cancelled":{"post":{"operationId":"webhookSubscriptionCancelled","tags":["Webhooks"],"summary":"A subscription was cancelled; it runs to the end of its period","description":"Posted to every endpoint that subscribed to `subscription.cancelled`, signed with its secret. Verify `webhook-signature` first.","security":[],"parameters":[{"$ref":"#/components/parameters/WebhookId"},{"$ref":"#/components/parameters/WebhookTimestamp"},{"$ref":"#/components/parameters/WebhookSignature"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WebhookEvent"},{"properties":{"type":{"const":"subscription.cancelled"},"data":{"$ref":"#/components/schemas/PurchaseEvent"}}}]}}}},"responses":{"2XX":{"description":"Received. Any other status, a redirect included, no answer within 5 seconds or no connection is a failed attempt, retried later. At most 4 KiB of the answer are read."}}}},"subscription.billing_issue":{"post":{"operationId":"webhookSubscriptionBillingIssue","tags":["Webhooks"],"summary":"The store could not charge a renewal","description":"Posted to every endpoint that subscribed to `subscription.billing_issue`, signed with its secret. Verify `webhook-signature` first.","security":[],"parameters":[{"$ref":"#/components/parameters/WebhookId"},{"$ref":"#/components/parameters/WebhookTimestamp"},{"$ref":"#/components/parameters/WebhookSignature"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WebhookEvent"},{"properties":{"type":{"const":"subscription.billing_issue"},"data":{"$ref":"#/components/schemas/PurchaseEvent"}}}]}}}},"responses":{"2XX":{"description":"Received. Any other status, a redirect included, no answer within 5 seconds or no connection is a failed attempt, retried later. At most 4 KiB of the answer are read."}}}},"subscription.expired":{"post":{"operationId":"webhookSubscriptionExpired","tags":["Webhooks"],"summary":"A subscription ended","description":"Posted to every endpoint that subscribed to `subscription.expired`, signed with its secret. Verify `webhook-signature` first.","security":[],"parameters":[{"$ref":"#/components/parameters/WebhookId"},{"$ref":"#/components/parameters/WebhookTimestamp"},{"$ref":"#/components/parameters/WebhookSignature"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WebhookEvent"},{"properties":{"type":{"const":"subscription.expired"},"data":{"$ref":"#/components/schemas/PurchaseEvent"}}}]}}}},"responses":{"2XX":{"description":"Received. Any other status, a redirect included, no answer within 5 seconds or no connection is a failed attempt, retried later. At most 4 KiB of the answer are read."}}}},"purchase.completed":{"post":{"operationId":"webhookPurchaseCompleted","tags":["Webhooks"],"summary":"A one-time purchase completed","description":"Posted to every endpoint that subscribed to `purchase.completed`, signed with its secret. Verify `webhook-signature` first.","security":[],"parameters":[{"$ref":"#/components/parameters/WebhookId"},{"$ref":"#/components/parameters/WebhookTimestamp"},{"$ref":"#/components/parameters/WebhookSignature"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WebhookEvent"},{"properties":{"type":{"const":"purchase.completed"},"data":{"$ref":"#/components/schemas/PurchaseEvent"}}}]}}}},"responses":{"2XX":{"description":"Received. Any other status, a redirect included, no answer within 5 seconds or no connection is a failed attempt, retried later. At most 4 KiB of the answer are read."}}}},"purchase.refunded":{"post":{"operationId":"webhookPurchaseRefunded","tags":["Webhooks"],"summary":"A purchase was refunded","description":"Posted to every endpoint that subscribed to `purchase.refunded`, signed with its secret. Verify `webhook-signature` first.","security":[],"parameters":[{"$ref":"#/components/parameters/WebhookId"},{"$ref":"#/components/parameters/WebhookTimestamp"},{"$ref":"#/components/parameters/WebhookSignature"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WebhookEvent"},{"properties":{"type":{"const":"purchase.refunded"},"data":{"$ref":"#/components/schemas/PurchaseEvent"}}}]}}}},"responses":{"2XX":{"description":"Received. Any other status, a redirect included, no answer within 5 seconds or no connection is a failed attempt, retried later. At most 4 KiB of the answer are read."}}}},"entitlements.changed":{"post":{"operationId":"webhookEntitlementsChanged","tags":["Webhooks"],"summary":"The active entitlements of a customer changed","description":"Posted to every endpoint that subscribed to `entitlements.changed`, signed with its secret. Verify `webhook-signature` first.","security":[],"parameters":[{"$ref":"#/components/parameters/WebhookId"},{"$ref":"#/components/parameters/WebhookTimestamp"},{"$ref":"#/components/parameters/WebhookSignature"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WebhookEvent"},{"properties":{"type":{"const":"entitlements.changed"},"data":{"$ref":"#/components/schemas/EntitlementsChanged"}}}]}}}},"responses":{"2XX":{"description":"Received. Any other status, a redirect included, no answer within 5 seconds or no connection is a failed attempt, retried later. At most 4 KiB of the answer are read."}}}},"webhook.test":{"post":{"operationId":"webhookWebhookTest","tags":["Webhooks"],"summary":"A test event sent from the console; it reports nothing","description":"Posted to every endpoint that subscribed to `webhook.test`, signed with its secret. Verify `webhook-signature` first.","security":[],"parameters":[{"$ref":"#/components/parameters/WebhookId"},{"$ref":"#/components/parameters/WebhookTimestamp"},{"$ref":"#/components/parameters/WebhookSignature"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WebhookEvent"},{"properties":{"type":{"const":"webhook.test"},"data":{"$ref":"#/components/schemas/WebhookTest"}}}]}}}},"responses":{"2XX":{"description":"Received. Any other status, a redirect included, no answer within 5 seconds or no connection is a failed attempt, retried later. At most 4 KiB of the answer are read."}}}}},"components":{"securitySchemes":{"serverKey":{"type":"http","scheme":"bearer","bearerFormat":"lt_srv_…","description":"A server key of the project, created by its owner in the console with fixed scopes, and an expiry of 90 days, 365 days or none. Scopes: `links:read`, `links:write`, `campaigns:read`, `campaigns:write`, `campaigns:send`, `messages:send`, `events:write`, `users:delete`, `purchases:read`."}},"parameters":{"IdempotencyKey":{"name":"Idempotency-Key","in":"header","required":false,"description":"Makes a retry safe: the same request with the same key within 24 hours gets the first answer again, with `Idempotent-Replayed: true`, and runs once. Keys are per server key; use a new one per request, e.g. a UUID.","schema":{"type":"string","minLength":1,"maxLength":255,"pattern":"^[\\x20-\\x7E]+$"}},"Cursor":{"name":"cursor","in":"query","required":false,"description":"`nextCursor` of the page before; leave out for the first.","schema":{"type":"string","pattern":"^[1-9][0-9]*$"}},"WebhookId":{"name":"webhook-id","in":"header","required":true,"description":"Id of the event, `msg_` and 32 hex digits, the same for every attempt and every resend: deduplicate on it.","schema":{"type":"string","pattern":"^msg_[0-9a-f]{32}$"}},"WebhookTimestamp":{"name":"webhook-timestamp","in":"header","required":true,"description":"Time of the attempt in Unix seconds.","schema":{"type":"string","pattern":"^[0-9]+$"}},"WebhookSignature":{"name":"webhook-signature","in":"header","required":true,"description":"Signatures separated by spaces, each `v1,` and base64 of HMAC-SHA256 over `{webhook-id}.{webhook-timestamp}.{body}`, keyed with the base64-decoded part of the secret after `whsec_`. After a rotation with a grace period the new secret signs first and the old one second, for 24 hours; accept the delivery when one of them matches.","schema":{"type":"string"}}},"headers":{"RetryAfter":{"description":"Seconds to wait before the next try.","schema":{"type":"integer","minimum":0}},"IdempotentReplayed":{"description":"`true` when the answer is the stored one of the `Idempotency-Key`.","schema":{"type":"string","enum":["true"]}}},"responses":{"Unauthorized":{"description":"No valid server key.","headers":{"WWW-Authenticate":{"description":"`Bearer`.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"},{"properties":{"error":{"enum":["unauthorized"]}}}]}}}},"Forbidden":{"description":"The key lacks the scope, or a browser called.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"},{"properties":{"error":{"enum":["forbidden"]}}}]}}}},"ForbiddenOrSuspended":{"description":"The key lacks the scope, a browser called, or the project is suspended.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"},{"properties":{"error":{"enum":["forbidden","project_suspended"]}}}]}}}},"PlanLimited":{"description":"The project is limited for its plan: nothing new is sent until it has a larger plan.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"},{"properties":{"error":{"enum":["plan_limited"]}}}]}}}},"InvalidInput":{"description":"The input is invalid; the message says why.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"},{"properties":{"error":{"enum":["invalid_input"]}}}]}}}},"NotFound":{"description":"Not found in the project of the key.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"},{"properties":{"error":{"enum":["not_found"]}}}]}}}},"MethodNotAllowed":{"description":"The path does not take this method.","headers":{"Allow":{"description":"The methods the path takes.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"},{"properties":{"error":{"enum":["method_not_allowed"]}}}]}}}},"Conflict":{"description":"The state of the resource forbids it, or the `Idempotency-Key` is used by another request, by one still running (then with `Retry-After`) or by one with an unknown outcome.","headers":{"Retry-After":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"},{"properties":{"error":{"enum":["conflict"]}}}]}}}},"PayloadTooLarge":{"description":"The body is over the limit of the operation.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"},{"properties":{"error":{"enum":["invalid_input"]}}}]}}}},"IdentityVerificationRequired":{"description":"No install of the project is verified as this user: set an identity secret, pass the user hash to `Lotse.identify`, or send to an `installId`.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"},{"properties":{"error":{"enum":["identity_verification_required"]}}}]}}}},"RateLimited":{"description":"A budget of calls or of the operation is used up.","headers":{"Retry-After":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"},{"properties":{"error":{"enum":["rate_limited"]}}}]}}}},"RateLimitedOrQuotaExceeded":{"description":"`rate_limited`: a budget is used up, see `Retry-After`. `quota_exceeded`: a quota of the project, without `Retry-After`.","headers":{"Retry-After":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"},{"properties":{"error":{"enum":["rate_limited","quota_exceeded"]}}}]}}}},"InternalError":{"description":"The server failed; try again later.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"},{"properties":{"error":{"enum":["internal_error"]}}}]}}}},"Unavailable":{"description":"Not finished now; the same request again does what is missing.","headers":{"Retry-After":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"},{"properties":{"error":{"enum":["unavailable"]}}}]}}}}},"schemas":{"Error":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","enum":["unauthorized","forbidden","project_suspended","plan_limited","invalid_input","not_found","method_not_allowed","conflict","identity_verification_required","quota_exceeded","rate_limited","internal_error","unavailable"]},"message":{"type":"string","description":"An English sentence for the developer. Never a stack trace, never a value of the request."}}},"Link":{"type":"object","required":["id","slug","url","targetPath","createdAt"],"properties":{"id":{"type":"integer"},"slug":{"type":"string"},"url":{"type":"string","format":"uri","description":"The full link, `https://<link domain>/l/<slug>`."},"targetPath":{"type":"string"},"campaign":{"type":"string"},"source":{"type":"string"},"medium":{"type":"string"},"fallbackIos":{"type":"string","format":"uri"},"fallbackAndroid":{"type":"string","format":"uri"},"fallbackWeb":{"type":"string","format":"uri"},"createdAt":{"type":"string","format":"date-time"}}},"LinkInput":{"type":"object","required":["slug","targetPath"],"additionalProperties":false,"properties":{"slug":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,64}$","description":"Letters, digits, \"-\" and \"_\", unique in the project; a taken one is `409`."},"targetPath":{"type":"string","minLength":1,"maxLength":2048,"pattern":"^/(?![/\\\\])","description":"The app route it opens, starting with one \"/\", e.g. `/offer/7`."},"campaign":{"type":["string","null"],"maxLength":100,"description":"Campaign the link belongs to, like `utm_campaign`. Empty or `null` for none."},"source":{"type":["string","null"],"maxLength":100,"description":"Source, like `utm_source`. Empty or `null` for none."},"medium":{"type":["string","null"],"maxLength":100,"description":"Medium, like `utm_medium`. Empty or `null` for none."},"fallbackIos":{"type":["string","null"],"format":"uri","maxLength":2048,"pattern":"^https://","description":"Where an iPhone goes when the project has no app with an App Store id. An https URL; empty or `null` for none."},"fallbackAndroid":{"type":["string","null"],"format":"uri","maxLength":2048,"pattern":"^https://","description":"Where an Android device goes when the project has no Android app. An https URL; empty or `null` for none."},"fallbackWeb":{"type":["string","null"],"format":"uri","maxLength":2048,"pattern":"^https://","description":"Where every other device goes, and iOS or Android without their own. An https URL; empty or `null` for none."}}},"Segment":{"type":"object","required":["id","name","predicate","createdAt"],"properties":{"id":{"type":"integer"},"name":{"type":"string","maxLength":64},"predicate":{"$ref":"#/components/schemas/SegmentPredicate"},"createdAt":{"type":"string","format":"date-time"}}},"SegmentPredicate":{"type":"object","required":["all"],"additionalProperties":false,"description":"Devices that match every condition. Each field at most once.","properties":{"all":{"type":"array","minItems":1,"items":{"$ref":"#/components/schemas/SegmentCondition"}}}},"SegmentCondition":{"type":"object","required":["field","op","value"],"description":"`platform` eq ios|android|web|other; `lastSeenDays` lte N (seen in the last N days, 1 to 3650); `appVersion` eq|ne \"1.2.0\"; `attributed` eq true|false (came from a Lotse link); `attributedCampaign` eq \"campaign\"; `didEvent`|`didNotEvent` eq \"event_name\" with `withinDays` N, optional `propKey` and `propValue`; `gaveFeedback` eq true|false, optional `category` and `withinDays`; `userTrait` eq \"value\" with `key`; `votedFor` eq the id of an idea; `surveyAnswer` eq the id of a survey, optional `group` or `choice` (index of the answer); `funnelDropout` eq K with `steps` (2 to 5 of `{\"name\", \"propKey\", \"propValue\"}`), `withinDays` and `conversionDays`: devices that reached step K but not step K+1; `entitlement` eq \"key\", optional `state` active|had|never (default active), of the customer: the device or the user it is verified as; `subscriptionStatus` eq pending|trial|active|cancelled|gracePeriod|billingRetry|paused|expired|refunded|revoked, optional `storeProductId` and `store`.","properties":{"field":{"type":"string","enum":["platform","lastSeenDays","appVersion","attributed","attributedCampaign","didEvent","didNotEvent","gaveFeedback","userTrait","votedFor","surveyAnswer","funnelDropout","entitlement","subscriptionStatus"]},"op":{"type":"string","enum":["eq","ne","lte"]},"value":{"type":["string","integer","boolean"]},"withinDays":{"type":"integer","minimum":1},"propKey":{"type":"string","maxLength":200},"propValue":{"type":"string","maxLength":200},"category":{"type":"string","enum":["bug","idea","praise","other","question"]},"key":{"type":"string","maxLength":64},"group":{"type":"string","enum":["detractor","passive","promoter"]},"choice":{"type":"integer","minimum":0},"steps":{"type":"array","items":{"type":"object"}},"conversionDays":{"type":"integer","minimum":1},"state":{"type":"string","enum":["active","had","never"]},"storeProductId":{"type":"string"},"store":{"type":"string","enum":["appStore","playStore","stripe"]}}},"Campaign":{"type":"object","required":["id","title","body","status","segment","createdAt"],"properties":{"id":{"type":"integer"},"title":{"type":"string"},"body":{"type":"string"},"status":{"type":"string","enum":["draft","scheduled","sending","sent","failed"]},"linkId":{"type":"integer","description":"The link whose route the push opens."},"segment":{"oneOf":[{"$ref":"#/components/schemas/SegmentPredicate"},{"type":"null"}],"description":"`null`: every device of the project."},"createdAt":{"type":"string","format":"date-time"},"scheduledAt":{"type":"string","format":"date-time","description":"When the send was handed to the push worker."},"sendAt":{"type":"string","format":"date-time","description":"The planned send time."},"sentAt":{"type":"string","format":"date-time"},"sendError":{"type":"string","description":"Why a scheduled campaign did not go out."}}},"CampaignInput":{"type":"object","required":["title","body"],"additionalProperties":false,"description":"Title, body and the route of the link together fit into one push: at most 3584 bytes as JSON. The audience is exactly one of `segmentId`, `segment` and `audience`.","properties":{"title":{"type":"string","minLength":1,"maxLength":200},"body":{"type":"string","minLength":1,"maxLength":1000},"linkId":{"type":["integer","null"],"minimum":1,"description":"A link of the project; the push opens its route."},"segmentId":{"type":["integer","null"],"minimum":1,"description":"A saved segment, see `GET /v1/segments`. The campaign copies its conditions."},"segment":{"oneOf":[{"$ref":"#/components/schemas/SegmentPredicate"},{"type":"null"}],"description":"Conditions of the audience, at least one."},"audience":{"type":["string","null"],"enum":["all",null],"description":"`all`: every device of the project."}}},"CampaignWithReport":{"allOf":[{"$ref":"#/components/schemas/Campaign"},{"type":"object","required":["report"],"properties":{"report":{"$ref":"#/components/schemas/CampaignReport"}}}]},"CampaignReport":{"type":"object","required":["deliveries","recipients","sent","opened","matured","measuredAt"],"properties":{"deliveries":{"$ref":"#/components/schemas/DeliverySummary"},"recipients":{"type":"integer","description":"Deliveries of the campaign."},"sent":{"type":"integer","description":"Deliveries the push provider accepted."},"opened":{"type":"integer","description":"Sent deliveries whose device opened the push."},"matured":{"type":"integer","description":"Sent deliveries whose 24 hours for the goal are over."},"goalEvent":{"type":"string"},"goal":{"type":"integer","description":"Matured deliveries whose device tracked `goalEvent` within 24 hours."},"openedGoal":{"type":"integer","description":"Of those, the ones that opened the push."},"measuredAt":{"type":"string","format":"date-time"}}},"DeliverySummary":{"type":"object","required":["pending","sending","sent","failed","invalidToken"],"description":"Deliveries, one per device, by status.","properties":{"pending":{"type":"integer"},"sending":{"type":"integer"},"sent":{"type":"integer"},"failed":{"type":"integer"},"invalidToken":{"type":"integer"}}},"ScheduleInput":{"type":"object","required":["sendAt"],"additionalProperties":false,"properties":{"sendAt":{"type":"string","format":"date-time","description":"With `Z` or an offset; at least 1 minute and at most 30 days ahead."}}},"EmptyInput":{"type":"object","additionalProperties":false,"description":"No fields; the body may be left out."},"Message":{"type":"object","required":["id","status","title","body","silent","createdAt","deliveries","opened"],"description":"Exactly one of `userId` and `installId` names the target.","properties":{"id":{"type":"integer"},"status":{"type":"string","enum":["queued","sent","failed","no_devices"],"description":"`queued` until every delivery ended; `sent` when the provider of at least one device accepted it; `failed` when none did; `no_devices` when the target had no device with a valid push token."},"userId":{"type":"string","description":"The id the app passed to `Lotse.identify`."},"installId":{"type":"string"},"title":{"type":["string","null"],"description":"`null` after 30 days."},"body":{"type":["string","null"],"description":"`null` after 30 days."},"targetPath":{"type":"string"},"silent":{"type":"boolean"},"badge":{"type":"integer","description":"`null` after 30 days."},"ttlSeconds":{"type":"integer"},"dataKeys":{"type":"array","items":{"type":"string"},"description":"The keys of `data`, sorted; the values are not returned. Left out after 30 days."},"createdAt":{"type":"string","format":"date-time"},"finishedAt":{"type":"string","format":"date-time"},"deliveries":{"$ref":"#/components/schemas/DeliverySummary"},"opened":{"type":"integer","description":"Sent deliveries whose device opened the push."}}},"MessageInput":{"type":"object","additionalProperties":false,"description":"Exactly one of `userId` and `installId`. `title` and `body` are required unless `silent`; a silent push has neither and no `targetPath`, but `data` or `badge`. Title, body, `targetPath` and `data` together fit into one push: at most 3584 bytes as JSON.","properties":{"userId":{"type":["string","null"],"minLength":1,"maxLength":256,"description":"The id the app passes to `Lotse.identify`. Only installs verified as this user get the push."},"installId":{"type":["string","null"],"pattern":"^[A-Za-z0-9._-]{1,64}$","description":"The install id the SDK reported to your backend."},"title":{"type":"string","minLength":1,"maxLength":200},"body":{"type":"string","minLength":1,"maxLength":1000},"targetPath":{"type":["string","null"],"minLength":1,"maxLength":2048,"pattern":"^/(?![/\\\\])","description":"The app route it opens, starting with one \"/\", e.g. `/offer/7`."},"data":{"type":["object","null"],"additionalProperties":{"type":"string"},"maxProperties":20,"propertyNames":{"pattern":"^[A-Za-z0-9_.-]{1,64}$"},"description":"Own keys for the app, at most 3072 bytes as JSON. Keys must not start with `lotse_`, `gcm.` or `google.`, and must not be `aps`, `from`, `notification`, `message_type` or `collapse_key`."},"badge":{"type":["integer","null"],"minimum":0,"maximum":99999,"description":"The app icon badge as an absolute number, 0 clears it. iOS shows it; Android shows it on a notification where the launcher supports counts, and a silent push carries it as `lotse_badge` in the data."},"silent":{"type":["boolean","null"],"description":"A push without a notification. Default `false`."},"ttlSeconds":{"type":["integer","null"],"minimum":0,"maximum":2419200,"description":"How long the providers keep the push for an offline device. 0 means now or never; without it their default applies."}}},"EventsInput":{"type":"object","required":["events"],"additionalProperties":false,"properties":{"events":{"type":"array","minItems":1,"maxItems":100,"items":{"$ref":"#/components/schemas/ServerEvent"}}}},"ServerEvent":{"type":"object","required":["name","userId"],"additionalProperties":false,"description":"One bad event refuses the whole call; the message names its index.","properties":{"name":{"type":"string","minLength":1,"maxLength":100,"not":{"enum":["attribution_match","billing_issue","billing_recovered","feature_unvoted","feature_voted","feedback_submitted","identify","link_click","product_changed","purchase_completed","purchase_refunded","purchase_transferred","push_open","push_sent","rage_tap","session_start","subscription_cancelled","subscription_expired","subscription_paused","subscription_renewed","subscription_started","subscription_uncancelled","survey_answered","trial_started"]},"description":"E.g. `purchase`. Names Lotse and its SDK write are refused."},"userId":{"type":"string","minLength":1,"maxLength":256,"description":"The id the app passes to `Lotse.identify`."},"ts":{"type":["string","null"],"format":"date-time","description":"When it happened, with `Z` or an offset; now without it. At most 30 days back and 5 minutes ahead."},"id":{"type":["string","null"],"pattern":"^[\\x21-\\x7E]{1,128}$","description":"Your id of the event, e.g. an order number: an event with an id the project has stored is skipped. Unique within the call."},"props":{"type":["object","null"],"maxProperties":20,"propertyNames":{"minLength":1,"maxLength":200},"additionalProperties":{"type":"string","maxLength":200},"description":"Text only. No personal data."}}},"EventsAccepted":{"type":"object","required":["accepted","duplicates"],"properties":{"accepted":{"type":"integer","description":"Events stored."},"duplicates":{"type":"integer","description":"Events skipped: the project had stored their id."}}},"WebhookEvent":{"type":"object","required":["type","timestamp","data"],"description":"The body of every delivery. Users and installs go by the ids your backend knows: `userId` as passed to `Lotse.identify`, `installId` as the SDK reports it. Never push tokens, install credentials or click pseudonyms.","properties":{"type":{"type":"string","enum":["feedback.created","feedback.replied","feature_request.created","survey.answered","install.attributed","campaign.finished","message.delivered","message.failed","user.identified","user.deleted","subscription.started","subscription.renewed","subscription.cancelled","subscription.billing_issue","subscription.expired","purchase.completed","purchase.refunded","entitlements.changed","webhook.test"]},"timestamp":{"type":"string","format":"date-time","description":"When the event happened."},"data":{"type":"object"}}},"FeedbackCreated":{"type":"object","required":["id","category","status","message","createdAt","consoleUrl"],"properties":{"id":{"type":"integer"},"category":{"type":"string","enum":["bug","idea","praise","other","question"]},"status":{"type":"string","enum":["open","inProgress","done","dismissed"]},"message":{"type":"string"},"contactEmail":{"type":"string","description":"The address the user left in the feedback form."},"lastScreen":{"type":"string"},"metadata":{"type":"object","additionalProperties":{"type":"string"}},"sessionId":{"type":"string"},"userId":{"type":"string"},"installId":{"type":"string"},"platform":{"type":"string","enum":["ios","android","web","other"]},"osVersion":{"type":"string"},"locale":{"type":"string"},"appVersion":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"consoleUrl":{"type":"string","format":"uri","description":"The feedback in the console, with screenshot, replay and conversation."},"screenshotUrl":{"type":"string","format":"uri","description":"The same page, set when a screenshot came along."}}},"FeedbackReplied":{"type":"object","required":["id","feedbackId","author","body","createdAt","consoleUrl"],"properties":{"id":{"type":"integer"},"feedbackId":{"type":"integer"},"author":{"type":"string","enum":["user","team"]},"body":{"type":"string"},"userId":{"type":"string"},"installId":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"consoleUrl":{"type":"string","format":"uri"}}},"FeatureRequestCreated":{"type":"object","required":["id","title","description","status","createdAt"],"properties":{"id":{"type":"integer"},"title":{"type":"string"},"description":{"type":"string"},"status":{"type":"string","enum":["open","planned","inProgress","shipped","archived"]},"sourceFeedbackId":{"type":"integer"},"createdAt":{"type":"string","format":"date-time"}}},"SurveyAnswered":{"type":"object","required":["surveyId","createdAt"],"properties":{"surveyId":{"type":"integer"},"kind":{"type":"string","enum":["nps","choice"]},"question":{"type":"string"},"score":{"type":"integer","description":"The score of an NPS survey."},"choice":{"type":"integer","description":"Index of the answer of a choice survey."},"choiceText":{"type":"string"},"comment":{"type":"string"},"userId":{"type":"string"},"installId":{"type":"string"},"appVersion":{"type":"string"},"createdAt":{"type":"string","format":"date-time"}}},"InstallAttributed":{"type":"object","required":["installId","platform","source","link","attributedAt"],"properties":{"installId":{"type":"string"},"userId":{"type":"string"},"platform":{"type":"string","enum":["ios","android","web","other"]},"source":{"type":"string","enum":["referrer","fingerprint"]},"link":{"$ref":"#/components/schemas/Link"},"attributedAt":{"type":"string","format":"date-time"}}},"CampaignFinished":{"allOf":[{"$ref":"#/components/schemas/Campaign"},{"type":"object","required":["deliveries"],"properties":{"deliveries":{"$ref":"#/components/schemas/DeliverySummary"}}}]},"UserIdentified":{"type":"object","required":["userId","installId","traits","verified"],"properties":{"userId":{"type":"string"},"installId":{"type":"string"},"traits":{"type":"object","additionalProperties":{"type":"string"},"description":"The traits of the user after this identify."},"verified":{"type":"boolean","description":"Whether a valid user hash came with it."}}},"UserDeleted":{"type":"object","required":["userId","deletedAt"],"properties":{"userId":{"type":"string"},"deletedAt":{"type":"string","format":"date-time"}}},"WebhookTest":{"type":"object","required":["message"],"properties":{"message":{"type":"string"}}},"Entitlement":{"type":"object","required":["key","source"],"description":"An active entitlement of a customer (docs/ENTITLEMENTS_CONTRACT.md). The fields after `expiresAt` only for `source` `purchase`.","properties":{"key":{"type":"string"},"source":{"type":"string","enum":["purchase","grant"]},"expiresAt":{"type":"string","format":"date-time","description":"End of access; left out when it does not end."},"store":{"type":"string","enum":["appStore","playStore","stripe"]},"storeProductId":{"type":"string"},"purchaseId":{"type":"integer","description":"Lotse id of the purchase, stable across renewals."},"environment":{"type":"string","enum":["production","sandbox"]},"status":{"type":"string","enum":["pending","trial","active","cancelled","gracePeriod","billingRetry","paused","expired","refunded","revoked"]},"willRenew":{"type":"boolean"},"periodType":{"type":"string","enum":["trial","intro","normal","promotional"]}}},"PurchaseEvent":{"type":"object","required":["event","purchase","entitlements","at"],"description":"Exactly one of `userId` (a verified user) and `installId` (a device) names the customer.","properties":{"userId":{"type":"string"},"installId":{"type":"string"},"event":{"type":"string","description":"The event in the stream, e.g. `trial_started` or `subscription_started` for `subscription.started`."},"purchase":{"type":"object","required":["purchaseId","store","storeProductId","kind","environment","status","periodType","willRenew","startedAt"],"properties":{"purchaseId":{"type":"integer"},"store":{"type":"string","enum":["appStore","playStore","stripe"]},"storeProductId":{"type":"string"},"kind":{"type":"string","enum":["subscription","nonConsumable","consumable"]},"environment":{"type":"string","enum":["production","sandbox"]},"status":{"type":"string","enum":["pending","trial","active","cancelled","gracePeriod","billingRetry","paused","expired","refunded","revoked"]},"periodType":{"type":"string","enum":["trial","intro","normal","promotional"]},"willRenew":{"type":"boolean"},"startedAt":{"type":"string","format":"date-time"},"currentPeriodEnd":{"type":"string","format":"date-time"},"graceUntil":{"type":"string","format":"date-time"},"cancelledAt":{"type":"string","format":"date-time"},"expiredAt":{"type":"string","format":"date-time"},"refundedAt":{"type":"string","format":"date-time"},"priceMicros":{"type":"integer"},"currency":{"type":"string"}}},"entitlements":{"type":"array","items":{"$ref":"#/components/schemas/Entitlement"},"description":"All active entitlements of the customer after it."},"at":{"type":"string","format":"date-time"}}},"EntitlementsChanged":{"type":"object","required":["entitlements","changedAt"],"description":"Exactly one of `userId` and `installId`. `entitlements` lists all active entitlements, an empty list means no access. Delivered at least once and in no fixed order: drop one older than the `changedAt` you have.","properties":{"userId":{"type":"string"},"installId":{"type":"string"},"entitlements":{"type":"array","items":{"$ref":"#/components/schemas/Entitlement"}},"changedAt":{"type":"string","format":"date-time"}}},"UserEntitlements":{"type":"object","required":["userId","entitlements","checkedAt"],"properties":{"userId":{"type":"string"},"entitlements":{"type":"array","items":{"$ref":"#/components/schemas/Entitlement"}},"checkedAt":{"type":"string","format":"date-time"}}}}}}