{
  "openapi": "3.1.0",
  "info": {
    "title": "ALIAS (alias-support), the user API",
    "version": "1",
    "description": "What a product's client calls: an anonymous identity the service makes, reports and their answers\n(live), a contact only if the user gives one, moving the identity to another device, and the user's\nown data. Every request carries the product's key (`X-Alias-Key`); all but three carry the user too\n(`X-Alias-User`, `X-Alias-Secret`, as `POST /api/v1/identity` or a transfer gave them).\n\nErrors are `{\"error\": \"…\"}` with a status: 400 (what's wrong, in words), 401 (no such key, id or\nsecret), 403 (blocked, or a browser origin the product doesn't allow), 404, 409, 413, 429 (a limit:\ntry later), 5xx. The clients follow this description: the same calls and names in each.\n",
    "license": {
      "name": "All rights reserved"
    }
  },
  "servers": [
    {
      "url": "https://alias.help"
    }
  ],
  "security": [
    {
      "key": [],
      "user": [],
      "secret": []
    }
  ],
  "components": {
    "securitySchemes": {
      "key": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Alias-Key",
        "description": "The product's API key: which product this is."
      },
      "user": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Alias-User",
        "description": "The user's id (a UUID v7 the service made)."
      },
      "secret": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Alias-Secret",
        "description": "This device's secret: 64 hex characters."
      }
    },
    "responses": {
      "Error": {
        "description": "Refused, and why.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "enum": [
              "key_revoked"
            ],
            "description": "What kind of refusal, when it's one a client acts on. key_revoked: this build's key was revoked."
          },
          "reason": {
            "type": "string",
            "maxLength": 300,
            "description": "key_revoked: support's words for the user (\"This version is no longer supported\"), when they gave some. Show it as it is."
          }
        }
      },
      "RemoteRequest": {
        "type": "object",
        "required": [
          "id",
          "ticket",
          "channel",
          "state",
          "asked_by",
          "asked_at",
          "deadline"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "ticket": {
            "type": "integer",
            "format": "int64"
          },
          "channel": {
            "type": "string",
            "enum": [
              "shell",
              "screen"
            ],
            "description": "shell: SSH to the device's sshd (its own sign-in still applies). screen: VNC; not encrypted end to end, the relay's operator could see it."
          },
          "state": {
            "type": "string",
            "enum": [
              "asked",
              "accepted",
              "open",
              "ended",
              "declined",
              "expired"
            ]
          },
          "asked_by": {
            "type": "string",
            "description": "The support person's name."
          },
          "asked_at": {
            "type": "string",
            "format": "date-time"
          },
          "deadline": {
            "type": "string",
            "format": "date-time",
            "description": "asked: answer by; accepted, open: the session's end."
          },
          "end_reason": {
            "type": "string",
            "description": "left, ended_by_support, ended_by_user, time_up, nobody_came, relay, not answered."
          },
          "relay": {
            "type": "object",
            "description": "The device's side, once accepted (answerRemote's answer only).",
            "required": [
              "url",
              "token"
            ],
            "properties": {
              "url": {
                "type": "string"
              },
              "token": {
                "type": "string"
              }
            }
          }
        }
      },
      "Identity": {
        "type": "object",
        "required": [
          "id",
          "secret"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "A UUID v7.",
            "example": "01a1a2b3-9cdf-71e1-b2d5-91d78e28a35a"
          },
          "secret": {
            "type": "string",
            "pattern": "^[0-9a-f]{64}$",
            "description": "Shown this once: keep it (only its hash is kept here)."
          }
        }
      },
      "Field": {
        "type": "object",
        "required": [
          "name",
          "type"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "label": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "text",
              "number",
              "bool",
              "geo",
              "link",
              "json"
            ]
          },
          "template": {
            "type": "string",
            "description": "A link's: {field} replaced by that field's value."
          }
        }
      },
      "Product": {
        "type": "object",
        "required": [
          "id",
          "name",
          "contact",
          "contact_kinds",
          "fields",
          "keeping"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "privacy_url": {
            "type": "string"
          },
          "branding": {
            "type": "object",
            "properties": {
              "logo": {
                "type": "string"
              },
              "colour": {
                "type": "string"
              },
              "accent": {
                "type": "string"
              }
            }
          },
          "fields": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Field"
            },
            "description": "What a report's context may carry; the rest is dropped."
          },
          "contact": {
            "type": "string",
            "enum": [
              "off",
              "optional"
            ]
          },
          "contact_consent": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/ConsentText"
              },
              {
                "type": "null"
              }
            ],
            "description": "With contact optional: the text to show by the contact field."
          },
          "contact_kinds": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "email",
                "other"
              ]
            }
          },
          "keeping": {
            "type": "object",
            "properties": {
              "done_days": {
                "type": "integer"
              },
              "unseen_days": {
                "type": "integer"
              }
            }
          },
          "redact": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RedactRule"
            },
            "description": "Applied, in order, to what the user sends, before it leaves the device; the service applies\nthem again as it comes in. Skip a rule that's `server_only` (hash) or a strategy the client\ndoesn't know; a `client:<name>` strategy is the app's own.\n"
          },
          "removable": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Paths (as a rule's) the user may take out before sending: show the telemetry, offer these."
          },
          "attachments": {
            "$ref": "#/components/schemas/AttachmentLimits"
          },
          "formats": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Format"
            }
          },
          "telemetry_max": {
            "type": "integer",
            "description": "The telemetry's bytes at most, as JSON."
          },
          "conversations": {
            "type": "object",
            "description": "How the product's tickets run.",
            "properties": {
              "mode": {
                "$ref": "#/components/schemas/Mode"
              },
              "switch": {
                "type": "string",
                "enum": [
                  "none",
                  "desk",
                  "user",
                  "both"
                ],
                "description": "Who may take a ticket live: support (desk), the user asking (user), or both."
              },
              "idle_minutes": {
                "type": "integer",
                "description": "A live ticket quiet this long goes back to tickets."
              }
            }
          },
          "live": {
            "type": "object",
            "description": "Live now, as the client should offer it.",
            "properties": {
              "mode": {
                "$ref": "#/components/schemas/Mode"
              },
              "user_may_ask": {
                "type": "boolean",
                "description": "Offer \"Talk live\" on a ticket (requestLive)."
              },
              "available": {
                "type": "boolean",
                "description": "Someone at the desk is taking live conversations now."
              }
            }
          }
        }
      },
      "RedactRule": {
        "type": "object",
        "required": [
          "path",
          "strategy",
          "server_only"
        ],
        "properties": {
          "path": {
            "type": "string",
            "description": "`/telemetry/…` (a JSON pointer, `*` for every key or element), `/context/<field>`, or `/note` (a note and a reply's text).",
            "example": "/telemetry/peers/*/ip"
          },
          "strategy": {
            "type": "string",
            "description": "drop, mask, truncate, round, coarsen, regex, hash (server only), or `client:<name>`."
          },
          "server_only": {
            "type": "boolean"
          },
          "keep": {
            "type": "integer",
            "description": "mask: ••••, then the last `keep` characters (leading • aren't counted)."
          },
          "max": {
            "type": "integer",
            "description": "truncate: the characters kept."
          },
          "step": {
            "type": "number",
            "description": "round: to the nearest multiple."
          },
          "digits": {
            "type": "integer",
            "description": "coarsen: a number, or a geo value's lat and lon, to these decimals."
          },
          "pattern": {
            "type": "string",
            "description": "regex: RE2, in every text under the path."
          },
          "replace": {
            "type": "string",
            "description": "regex: what a match becomes, literally ([redacted])."
          }
        }
      },
      "AttachmentLimits": {
        "type": "object",
        "required": [
          "max",
          "bytes",
          "types"
        ],
        "properties": {
          "max": {
            "type": "integer",
            "description": "Files a report or a reply may carry."
          },
          "bytes": {
            "type": "integer",
            "description": "Their bytes together."
          },
          "types": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "image/jpeg",
                "image/png",
                "image/webp",
                "text/plain",
                "application/json"
              ]
            },
            "description": "Told from the bytes, not the name or the part's type."
          }
        }
      },
      "Attachment": {
        "type": "object",
        "required": [
          "id",
          "name",
          "type",
          "size",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "name": {
            "type": "string",
            "description": "Letters, digits, . - _; unique in its report or reply: what ![](attachment:name) names."
          },
          "type": {
            "type": "string"
          },
          "size": {
            "type": "integer"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Format": {
        "type": "string",
        "enum": [
          "text/plain",
          "text/markdown"
        ],
        "description": "text/markdown: CommonMark with GFM's tables, strikethrough and autolinks; no raw HTML, and no\nimages but the report's own attachments (`![](attachment:name)`). Render it, or show it as it is.\n"
      },
      "Me": {
        "type": "object",
        "required": [
          "id",
          "created_at",
          "devices"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "devices": {
            "type": "integer",
            "description": "Devices holding a secret for this id."
          },
          "contact": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Contact"
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "Contact": {
        "type": "object",
        "required": [
          "kind",
          "value",
          "given_at"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "email",
              "other"
            ]
          },
          "value": {
            "type": "string"
          },
          "given_at": {
            "type": "string",
            "format": "date-time"
          },
          "consent": {
            "$ref": "#/components/schemas/Consent"
          }
        }
      },
      "ConsentText": {
        "type": "object",
        "required": [
          "text",
          "version"
        ],
        "properties": {
          "text": {
            "type": "string",
            "description": "Show it, whole, where the user gives a contact; send nothing until they agree to it."
          },
          "version": {
            "type": "string",
            "description": "The text's hash: send it back with the contact, as proof of which text was shown."
          }
        }
      },
      "Consent": {
        "type": "object",
        "required": [
          "id",
          "purpose",
          "version",
          "text",
          "kind",
          "given_at"
        ],
        "description": "What the user agreed to in giving a contact, as recorded. Kept, ended or not, until the identity goes.",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "purpose": {
            "type": "string",
            "enum": [
              "contact"
            ]
          },
          "version": {
            "type": "string"
          },
          "text": {
            "type": "string",
            "description": "The text as it was when they agreed."
          },
          "kind": {
            "type": "string",
            "enum": [
              "email",
              "other"
            ]
          },
          "app_version": {
            "type": "string"
          },
          "given_at": {
            "type": "string",
            "format": "date-time"
          },
          "ended_at": {
            "type": "string",
            "format": "date-time"
          },
          "ended": {
            "type": "string",
            "enum": [
              "withdrawn",
              "replaced"
            ]
          }
        }
      },
      "ContactState": {
        "type": "object",
        "required": [
          "contact"
        ],
        "properties": {
          "contact": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Contact"
              },
              {
                "type": "null"
              }
            ]
          },
          "policy": {
            "type": "string",
            "enum": [
              "off",
              "optional"
            ]
          },
          "kinds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "consent": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/ConsentText"
              },
              {
                "type": "null"
              }
            ],
            "description": "The text to show now (as the product's contact_consent)."
          }
        }
      },
      "Report": {
        "type": "object",
        "required": [
          "note"
        ],
        "properties": {
          "note": {
            "type": "string",
            "minLength": 1,
            "maxLength": 4000
          },
          "format": {
            "$ref": "#/components/schemas/Format"
          },
          "screenshot": {
            "type": "string",
            "contentEncoding": "base64",
            "description": "A JPEG, up to 3 MB (JSON only: as multipart, send it as an attachment)."
          },
          "context": {
            "type": "object",
            "additionalProperties": true,
            "description": "The product's fields (GET /api/v1/product)."
          },
          "app_version": {
            "type": "string",
            "maxLength": 40
          },
          "telemetry": {
            "type": "object",
            "additionalProperties": true,
            "description": "Free JSON up to the product's size, matching its schema if it has one (else 400, saying where)."
          }
        }
      },
      "Reply": {
        "type": "object",
        "required": [
          "body"
        ],
        "properties": {
          "body": {
            "type": "string",
            "minLength": 1,
            "maxLength": 4000
          },
          "format": {
            "$ref": "#/components/schemas/Format"
          },
          "screenshot": {
            "type": "string",
            "contentEncoding": "base64"
          },
          "context": {
            "type": "object",
            "additionalProperties": true
          },
          "telemetry": {
            "type": "object",
            "additionalProperties": true
          }
        }
      },
      "Entry": {
        "type": "object",
        "required": [
          "id",
          "author",
          "kind",
          "body",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "author": {
            "type": "string",
            "enum": [
              "user",
              "support"
            ]
          },
          "name": {
            "type": "string",
            "description": "A support person's name."
          },
          "agent": {
            "type": "boolean",
            "description": "Support's, but written by an agent (a service account: an AI agent, a script), not a person. Show the user it was."
          },
          "kind": {
            "type": "string",
            "enum": [
              "comment",
              "status",
              "mode"
            ],
            "description": "status: body is the new status; mode: the ticket's new mode."
          },
          "body": {
            "type": "string"
          },
          "format": {
            "$ref": "#/components/schemas/Format"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "context": {
            "type": "object",
            "additionalProperties": true
          },
          "attachments": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Attachment"
            }
          },
          "has_screenshot": {
            "type": "boolean",
            "description": "An image among the attachments."
          }
        }
      },
      "Status": {
        "type": "string",
        "enum": [
          "open",
          "in_progress",
          "waiting",
          "resolved",
          "closed"
        ]
      },
      "Mode": {
        "type": "string",
        "enum": [
          "tickets",
          "live"
        ],
        "description": "tickets: request and response; live: a conversation, as it happens."
      },
      "Ticket": {
        "type": "object",
        "required": [
          "id",
          "product",
          "status",
          "note",
          "format",
          "app_version",
          "attachments",
          "has_screenshot",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "product": {
            "type": "string"
          },
          "status": {
            "$ref": "#/components/schemas/Status"
          },
          "note": {
            "type": "string"
          },
          "format": {
            "$ref": "#/components/schemas/Format"
          },
          "context": {
            "type": "object",
            "additionalProperties": true
          },
          "app_version": {
            "type": "string"
          },
          "attachments": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Attachment"
            },
            "description": "The report's own (a reply's are its entry's)."
          },
          "has_screenshot": {
            "type": "boolean",
            "description": "An image among the attachments."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "support_at": {
            "type": "string",
            "format": "date-time",
            "description": "Support's last answer or status change."
          },
          "mode": {
            "$ref": "#/components/schemas/Mode"
          },
          "live_requested_at": {
            "type": "string",
            "format": "date-time",
            "description": "The user asked for live and nobody was there yet."
          },
          "user_read_at": {
            "type": "string",
            "format": "date-time",
            "description": "The user read the thread up to then."
          },
          "support_read_at": {
            "type": "string",
            "format": "date-time",
            "description": "Support read the thread up to then."
          },
          "thread": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Entry"
            }
          }
        }
      },
      "Update": {
        "type": "object",
        "required": [
          "kind",
          "ticket",
          "status",
          "at"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "answer",
              "mode",
              "typing",
              "read",
              "remote"
            ],
            "description": "Polling (updates) has answers only; the stream has all five. remote: a remote assistance request changed (look it up: remoteRequests)."
          },
          "ticket": {
            "type": "integer",
            "format": "int64"
          },
          "status": {
            "$ref": "#/components/schemas/Status"
          },
          "at": {
            "type": "string",
            "format": "date-time"
          },
          "mode": {
            "$ref": "#/components/schemas/Mode"
          },
          "who": {
            "type": "string",
            "enum": [
              "support"
            ],
            "description": "typing, read: who."
          },
          "name": {
            "type": "string",
            "description": "typing: the support person's name."
          },
          "remote": {
            "type": "string",
            "description": "remote: the request's id."
          },
          "channel": {
            "type": "string",
            "enum": [
              "shell",
              "screen"
            ],
            "description": "remote: which."
          },
          "state": {
            "type": "string",
            "description": "remote: what it is now (RemoteRequest's state)."
          }
        }
      },
      "Transfer": {
        "type": "object",
        "required": [
          "code",
          "expires_at"
        ],
        "properties": {
          "code": {
            "type": "string",
            "pattern": "^[0-9A-HJKMNP-TV-Z]{4}-[0-9A-HJKMNP-TV-Z]{4}$",
            "description": "Typed on the other device (any case; O, I, L read as 0, 1, 1)."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "WebhookEvent": {
        "description": "What a webhook endpoint receives (Standard Webhooks): signed with the webhook-id, webhook-timestamp\nand webhook-signature headers. The fields marked \"content\" are there only if the endpoint has\ncontent on, and are redacted by the product's rules.\n",
        "type": "object",
        "required": [
          "type",
          "timestamp",
          "data"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "ticket.created",
              "ticket.replied",
              "ticket.answered",
              "ticket.status_changed",
              "ticket.deleted",
              "ticket.live_started",
              "ticket.live_ended",
              "webhook.test"
            ]
          },
          "timestamp": {
            "type": "string",
            "format": "date-time"
          },
          "data": {
            "type": "object",
            "required": [
              "product"
            ],
            "properties": {
              "product": {
                "type": "string"
              },
              "ticket": {
                "$ref": "#/components/schemas/WebhookTicket"
              },
              "entry": {
                "$ref": "#/components/schemas/WebhookEntry"
              },
              "webhook": {
                "type": "string",
                "description": "webhook.test only"
              },
              "by": {
                "type": "string",
                "description": "webhook.test only: who sent it"
              }
            }
          }
        }
      },
      "WebhookTicket": {
        "type": "object",
        "required": [
          "id",
          "status",
          "mode",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "status": {
            "type": "string",
            "enum": [
              "open",
              "in_progress",
              "waiting",
              "resolved",
              "closed"
            ]
          },
          "mode": {
            "type": "string",
            "enum": [
              "tickets",
              "live"
            ]
          },
          "app_version": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "the ticket on the panel (with the deployment's public_url)"
          },
          "user": {
            "type": "string",
            "description": "content: the user's anonymous id"
          },
          "note": {
            "type": "string",
            "description": "content"
          },
          "format": {
            "type": "string",
            "enum": [
              "text/plain",
              "text/markdown"
            ],
            "description": "content"
          },
          "context": {
            "type": "object",
            "description": "content"
          },
          "telemetry": {
            "type": "object",
            "description": "content"
          },
          "attachments": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WebhookFile"
            },
            "description": "content"
          }
        }
      },
      "WebhookEntry": {
        "description": "What happened (ticket.replied, .answered, .status_changed, .live_started, .live_ended).",
        "type": "object",
        "required": [
          "id",
          "author",
          "kind",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "author": {
            "type": "string",
            "enum": [
              "user",
              "support"
            ]
          },
          "kind": {
            "type": "string",
            "enum": [
              "comment",
              "status",
              "mode"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "status": {
            "type": "string",
            "description": "kind status: the new status"
          },
          "mode": {
            "type": "string",
            "enum": [
              "tickets",
              "live"
            ],
            "description": "kind mode: the new mode"
          },
          "body": {
            "type": "string",
            "description": "content"
          },
          "format": {
            "type": "string",
            "description": "content"
          },
          "name": {
            "type": "string",
            "description": "content: the support person's name, on an answer"
          },
          "agent": {
            "type": "boolean",
            "description": "by an agent (a service account), not a person"
          },
          "attachments": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WebhookFile"
            },
            "description": "content"
          }
        }
      },
      "WebhookFile": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "type": {
            "type": "string"
          },
          "size": {
            "type": "integer"
          }
        }
      }
    }
  },
  "paths": {
    "/api/v1/product": {
      "get": {
        "operationId": "getProduct",
        "summary": "The product's public settings (for the client's UI)",
        "security": [
          {
            "key": []
          }
        ],
        "responses": {
          "200": {
            "description": "The product",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Product"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/v1/identity": {
      "post": {
        "operationId": "newIdentity",
        "summary": "A new anonymous identity, made by the service (or a new secret for an id support reset)",
        "description": "Limited to 20 an hour per address per product, and by a budget per key. If the product asks for\nproof of work (`identityChallenge` says how many bits), the request carries `X-Alias-Work`:\n`<challenge>:<nonce>`, where SHA-256 of that whole value begins with that many zero bits. Without\nit, or with a challenge out of time or used: 428 (ask for another challenge).\n",
        "security": [
          {
            "key": []
          }
        ],
        "parameters": [
          {
            "name": "X-Alias-Work",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "`<challenge>:<nonce>`, when the product asks for proof of work."
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string",
                    "description": "An id whose secrets support reset."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The reset id's new secret",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Identity"
                }
              }
            }
          },
          "201": {
            "description": "A new id",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Identity"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/v1/identity/challenge": {
      "get": {
        "operationId": "identityChallenge",
        "summary": "The proof of work a new identity costs, if any (a challenge good for five minutes, once)",
        "description": "`bits` 0: none, post the identity as it is. Otherwise find a nonce such that SHA-256 of\n`<challenge>:<nonce>` begins with `bits` zero bits (about 2^bits tries), and send that as\n`X-Alias-Work`. The bits can go up at any time (an attack, a key in quarantine), so a client\nalways asks.\n",
        "security": [
          {
            "key": []
          }
        ],
        "responses": {
          "200": {
            "description": "The work asked for",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "bits"
                  ],
                  "properties": {
                    "bits": {
                      "type": "integer",
                      "minimum": 0,
                      "maximum": 22
                    },
                    "challenge": {
                      "type": "string"
                    },
                    "expires_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/v1/transfer/claim": {
      "post": {
        "operationId": "claimTransfer",
        "summary": "This device gets the identity a transfer code names, with a secret of its own",
        "description": "10 tries an hour per address per product; a code ends after 5 wrong tries, or 10 minutes.",
        "security": [
          {
            "key": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "code"
                ],
                "properties": {
                  "code": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The identity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Identity"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/v1/me": {
      "get": {
        "operationId": "me",
        "summary": "The id and secret are good; the id's devices and contact",
        "responses": {
          "200": {
            "description": "Yes",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Me"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "delete": {
        "operationId": "eraseMe",
        "summary": "Erase the id and everything of it (reports, attachments, contact), for good",
        "responses": {
          "200": {
            "description": "Erased",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "erased": {
                      "type": "string"
                    },
                    "tickets": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/v1/me/data": {
      "get": {
        "operationId": "myData",
        "summary": "A copy of everything kept about the id, as JSON (attachments base64)",
        "responses": {
          "200": {
            "description": "The copy",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/v1/me/contact": {
      "get": {
        "operationId": "getContact",
        "summary": "The contact given (null none), and the product's policy",
        "responses": {
          "200": {
            "description": "The state",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactState"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "put": {
        "operationId": "setContact",
        "summary": "Give (or change) a contact, with the user's consent, if the product takes one (else 409)",
        "description": "Only after the user agreed to the product's `contact_consent` text, shown whole: `consent.version`\nis that text's version. Without it 400; with an older text's 409 (show the current one and ask\nagain). The consent is recorded with the text; a new contact ends the one before (replaced).\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "kind",
                  "value",
                  "consent"
                ],
                "properties": {
                  "kind": {
                    "type": "string",
                    "enum": [
                      "email",
                      "other"
                    ]
                  },
                  "value": {
                    "type": "string",
                    "maxLength": 254
                  },
                  "consent": {
                    "type": "object",
                    "required": [
                      "version"
                    ],
                    "properties": {
                      "version": {
                        "type": "string",
                        "description": "contact_consent.version, as shown."
                      },
                      "app_version": {
                        "type": "string",
                        "maxLength": 40
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Given",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactState"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "delete": {
        "operationId": "withdrawContact",
        "summary": "Withdraw the contact (deleted at once; its consent's record kept, ended, until the identity goes)",
        "responses": {
          "200": {
            "description": "Gone",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactState"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/v1/me/transfer": {
      "post": {
        "operationId": "newTransfer",
        "summary": "A code to type on another device within 10 minutes",
        "responses": {
          "201": {
            "description": "The code",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Transfer"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/v1/tickets": {
      "post": {
        "operationId": "report",
        "summary": "Send a report",
        "description": "JSON, or multipart/form-data with files (the product's attachments limits and types).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Report"
              }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "report"
                ],
                "properties": {
                  "report": {
                    "$ref": "#/components/schemas/Report"
                  },
                  "attachment": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "binary"
                    },
                    "description": "Each file a part of its own, with its filename. Images lose their metadata (EXIF, GPS, text) here."
                  }
                }
              },
              "encoding": {
                "report": {
                  "contentType": "application/json"
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The ticket",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ticket"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "get": {
        "operationId": "tickets",
        "summary": "The user's tickets, newest first (50), without their threads",
        "responses": {
          "200": {
            "description": "The list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "tickets"
                  ],
                  "properties": {
                    "tickets": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Ticket"
                      }
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/v1/tickets/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "format": "int64"
          }
        }
      ],
      "get": {
        "operationId": "ticket",
        "summary": "One ticket, with its thread",
        "responses": {
          "200": {
            "description": "The ticket",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ticket"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/v1/tickets/{id}/comments": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "format": "int64"
          }
        }
      ],
      "post": {
        "operationId": "reply",
        "summary": "Reply (a ticket waiting on the user, or resolved, opens again; a closed one is 409)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Reply"
              }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "report"
                ],
                "properties": {
                  "report": {
                    "$ref": "#/components/schemas/Reply"
                  },
                  "attachment": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "binary"
                    },
                    "description": "Each file a part of its own, with its filename. Images lose their metadata (EXIF, GPS, text) here."
                  }
                }
              },
              "encoding": {
                "report": {
                  "contentType": "application/json"
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The ticket",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ticket"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/v1/tickets/{id}/live": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "format": "int64"
          }
        }
      ],
      "post": {
        "operationId": "requestLive",
        "summary": "Ask to talk about the ticket live",
        "description": "If someone at the desk is taking live conversations, the ticket goes live at once (`mode` live);\notherwise it's marked (`live_requested_at`) and goes live when support takes it. 403 when the\nproduct doesn't let users ask (`live.user_may_ask`).\n",
        "responses": {
          "200": {
            "description": "The ticket",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ticket"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/v1/tickets/{id}/typing": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "format": "int64"
          }
        }
      ],
      "post": {
        "operationId": "typing",
        "summary": "The user is typing (a live ticket's; at most one every 2 s is passed on)",
        "responses": {
          "204": {
            "description": "Noted"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/v1/tickets/{id}/read": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "format": "int64"
          }
        }
      ],
      "post": {
        "operationId": "markRead",
        "summary": "The user has read the thread up to now (support sees it)",
        "responses": {
          "204": {
            "description": "Noted"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/v1/tickets/{id}/attachments/{aid}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "format": "int64"
          }
        },
        {
          "name": "aid",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "format": "int64"
          }
        }
      ],
      "get": {
        "operationId": "attachment",
        "summary": "One of the user's own attachments, as kept",
        "responses": {
          "200": {
            "description": "The file",
            "content": {
              "*/*": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/v1/remote": {
      "get": {
        "operationId": "remoteRequests",
        "summary": "The user's remote assistance requests still going, across their tickets",
        "description": "What a device's agent watches (with the `remote` events): support asked to open a shell (SSH) or see\nthe screen (VNC) on this device. Nothing connects until the device accepts. `offered`: the product\noffers remote assistance at all on this deployment.\n",
        "responses": {
          "200": {
            "description": "The requests",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "remote",
                    "offered"
                  ],
                  "properties": {
                    "remote": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/RemoteRequest"
                      }
                    },
                    "offered": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/v1/tickets/{id}/remote": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "format": "int64"
          }
        }
      ],
      "get": {
        "operationId": "ticketRemote",
        "summary": "A ticket's remote assistance requests, all of them, newest first",
        "responses": {
          "200": {
            "description": "The requests",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "remote",
                    "offered"
                  ],
                  "properties": {
                    "remote": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/RemoteRequest"
                      }
                    },
                    "offered": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/v1/tickets/{id}/remote/{rid}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer",
            "format": "int64"
          }
        },
        {
          "name": "rid",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "operationId": "answerRemote",
        "summary": "The device's answer to a request",
        "description": "Yes: the relay's session is made and `relay` is this device's side of it: dial `url` (a WebSocket)\nwith `token` as the bearer and connect it to what the channel is on this device (the device's own\nsetting: 127.0.0.1:22 for shell, :5900 for screen), never anything support names. The session is\nsingle attach: when either side leaves it's over. Asking yes again before anyone's on it gives the\nsame again. 409: it's not waiting for an answer any more.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "accept"
                ],
                "properties": {
                  "accept": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The request, as it is now (with `relay` when accepted)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RemoteRequest"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "delete": {
        "operationId": "endRemote",
        "summary": "The device ends the session (or takes its yes back)",
        "responses": {
          "200": {
            "description": "Ended"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/v1/updates": {
      "get": {
        "operationId": "updates",
        "summary": "The tickets support answered or changed since a time (polling)",
        "description": "`now` is the time to ask from next, by the service's clock and a moment early, so an update can\ncome twice (by poll and stream, or two polls): drop what's been seen, by ticket and `at`.\n",
        "parameters": [
          {
            "name": "since",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "The last answer's `now`; none: everything support ever touched."
          }
        ],
        "responses": {
          "200": {
            "description": "Updates, and the time to ask from next",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "updates",
                    "now"
                  ],
                  "properties": {
                    "updates": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Update"
                      }
                    },
                    "now": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/v1/events": {
      "get": {
        "operationId": "events",
        "summary": "The same, pushed (Server-Sent Events) while open",
        "description": "An Update as `data`, its kind the `event`: `answer`; on a live ticket also `mode` (it went live or\nback), `typing` (support is typing) and `read` (support read up to `at`); `remote` when a remote\nassistance request is asked or changes. A comment line every 25 s.\nAt most 3 streams per user.\nA client that can't send headers on a stream (a browser's EventSource) polls /api/v1/updates instead.\n",
        "responses": {
          "200": {
            "description": "The stream",
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    }
  },
  "webhooks": {
    "ticket.created": {
      "post": {
        "summary": "A user reported a problem.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "410": {
            "description": "The endpoint is turned off."
          },
          "2XX": {
            "description": "Delivered."
          },
          "default": {
            "description": "Retried later."
          }
        }
      }
    },
    "ticket.replied": {
      "post": {
        "summary": "A user replied on a ticket.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "410": {
            "description": "The endpoint is turned off."
          },
          "2XX": {
            "description": "Delivered."
          },
          "default": {
            "description": "Retried later."
          }
        }
      }
    },
    "ticket.answered": {
      "post": {
        "summary": "Support answered.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "410": {
            "description": "The endpoint is turned off."
          },
          "2XX": {
            "description": "Delivered."
          },
          "default": {
            "description": "Retried later."
          }
        }
      }
    },
    "ticket.status_changed": {
      "post": {
        "summary": "Support changed the status.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "410": {
            "description": "The endpoint is turned off."
          },
          "2XX": {
            "description": "Delivered."
          },
          "default": {
            "description": "Retried later."
          }
        }
      }
    },
    "ticket.deleted": {
      "post": {
        "summary": "Support deleted a ticket.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "410": {
            "description": "The endpoint is turned off."
          },
          "2XX": {
            "description": "Delivered."
          },
          "default": {
            "description": "Retried later."
          }
        }
      }
    },
    "ticket.live_started": {
      "post": {
        "summary": "A ticket became a live conversation.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "410": {
            "description": "The endpoint is turned off."
          },
          "2XX": {
            "description": "Delivered."
          },
          "default": {
            "description": "Retried later."
          }
        }
      }
    },
    "ticket.live_ended": {
      "post": {
        "summary": "A live ticket went back to an ordinary one.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "410": {
            "description": "The endpoint is turned off."
          },
          "2XX": {
            "description": "Delivered."
          },
          "default": {
            "description": "Retried later."
          }
        }
      }
    },
    "webhook.test": {
      "post": {
        "summary": "Sent by hand from the panel, to every endpoint.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "410": {
            "description": "The endpoint is turned off."
          },
          "2XX": {
            "description": "Delivered."
          },
          "default": {
            "description": "Retried later."
          }
        }
      }
    }
  }
}
