{
  "openapi": "3.1.0",
  "info": {
    "title": "Escalio Customer API",
    "version": "1",
    "description": "Customer requests only. Use the exact portal origin where the key was created. Every verified customer creates their own keys in the ordinary portal \u2192 API keys; no iframe or JWT setup required. Company admins manage all company keys and may disable or enable them. Request company administrator access from support. This contract covers API-key operations; embedded session exchange and UI-only key management are separate."
  },
  "servers": [
    {
      "url": "https://{portal}",
      "variables": {
        "portal": {
          "default": "YOUR-PORTAL.escalio.org",
          "description": "The exact support portal host used to create the key, including a custom domain if applicable."
        }
      }
    }
  ],
  "security": [
    {
      "CustomerKey": []
    }
  ],
  "components": {
    "securitySchemes": {
      "CustomerKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "esc_customer_\u2026"
      }
    },
    "schemas": {
      "NewTicket": {
        "additionalProperties": false,
        "properties": {
          "body": {
            "maxLength": 20000,
            "minLength": 1,
            "pattern": "^[^\\x00]+$",
            "title": "Body",
            "type": "string"
          },
          "format": {
            "default": "markdown",
            "enum": [
              "plain",
              "markdown"
            ],
            "title": "Format",
            "type": "string"
          },
          "subject": {
            "maxLength": 240,
            "minLength": 1,
            "pattern": "^[^\\x00-\\x1f\\x7f]+$",
            "title": "Subject",
            "type": "string"
          }
        },
        "required": [
          "body",
          "subject"
        ],
        "title": "NewTicket",
        "type": "object"
      },
      "Message": {
        "additionalProperties": false,
        "properties": {
          "body": {
            "maxLength": 20000,
            "minLength": 1,
            "pattern": "^[^\\x00]+$",
            "title": "Body",
            "type": "string"
          },
          "format": {
            "default": "markdown",
            "enum": [
              "plain",
              "markdown"
            ],
            "title": "Format",
            "type": "string"
          }
        },
        "required": [
          "body"
        ],
        "title": "Message",
        "type": "object"
      }
    }
  },
  "paths": {
    "/api/portal/customer/v1/me": {
      "get": {
        "operationId": "identity",
        "summary": "Current customer and scopes",
        "description": "No ticket scope is required. Returns contact and company IDs, role, kind and scopes.",
        "parameters": [],
        "responses": {
          "200": {
            "description": "No ticket scope is required. Returns contact and company IDs, role, kind and scopes."
          },
          "400": {
            "description": "Malformed request. Fix the request before retrying."
          },
          "401": {
            "description": "The key is invalid, expired, revoked or its sign-in or company access changed. Sign in to the portal and replace it."
          },
          "403": {
            "description": "The key lacks the required scope, the customer role does not allow this action, or portal terms must be accepted."
          },
          "404": {
            "description": "This ticket is not available to this customer in this portal."
          },
          "409": {
            "description": "This Idempotency-Key already belongs to a different operation or payload. Check your saved request."
          },
          "422": {
            "description": "Invalid fields, UUID, page, or missing Idempotency-Key. Correct the request."
          },
          "429": {
            "description": "Too many requests. Wait at least a minute and retry with backoff."
          },
          "503": {
            "description": "Support is temporarily unavailable. Retry with backoff and the same idempotency key for writes."
          }
        },
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "cURL (POSIX shell)",
            "source": "curl --fail-with-body --silent --show-error --request GET \\\n  \"$ESCALIO_PORTAL/api/portal/customer/v1/me\" \\\n  --header \"Authorization: Bearer $ESCALIO_CUSTOMER_KEY\""
          }
        ]
      }
    },
    "/api/portal/customer/v1/tickets": {
      "get": {
        "operationId": "list",
        "summary": "List requests",
        "description": "Requires read. mine is the default; company requires a company admin. Returns page, page_size (50) and tickets; no total. Request another page while it contains 50 items.",
        "parameters": [
          {
            "name": "view",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "mine",
                "company"
              ],
              "default": "mine"
            },
            "description": "mine (default) returns your requests; company returns all accessible company requests and requires a company admin. Example: ?view=company&page=1."
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000000,
              "default": 1
            },
            "description": "Optional page number, 1\u20131,000,000; defaults to 1. Use ?page=2 to read the next page."
          }
        ],
        "responses": {
          "200": {
            "description": "Requires read. mine is the default; company requires a company admin. Returns page, page_size (50) and tickets; no total. Request another page while it contains 50 items."
          },
          "400": {
            "description": "Malformed request. Fix the request before retrying."
          },
          "401": {
            "description": "The key is invalid, expired, revoked or its sign-in or company access changed. Sign in to the portal and replace it."
          },
          "403": {
            "description": "The key lacks the required scope, the customer role does not allow this action, or portal terms must be accepted."
          },
          "404": {
            "description": "This ticket is not available to this customer in this portal."
          },
          "409": {
            "description": "This Idempotency-Key already belongs to a different operation or payload. Check your saved request."
          },
          "422": {
            "description": "Invalid fields, UUID, page, or missing Idempotency-Key. Correct the request."
          },
          "429": {
            "description": "Too many requests. Wait at least a minute and retry with backoff."
          },
          "503": {
            "description": "Support is temporarily unavailable. Retry with backoff and the same idempotency key for writes."
          }
        },
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "cURL (POSIX shell)",
            "source": "curl --fail-with-body --silent --show-error --request GET \\\n  \"$ESCALIO_PORTAL/api/portal/customer/v1/tickets\" \\\n  --header \"Authorization: Bearer $ESCALIO_CUSTOMER_KEY\""
          }
        ]
      },
      "post": {
        "operationId": "create",
        "summary": "Create a request",
        "description": "Requires create. Creates a public customer request as the key\u2019s contact. Returns 201 and an id, including on an identical retry. Customer identity comes from the key, not the payload.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 8,
              "maxLength": 100
            },
            "description": "Persist a unique ID for this operation; reuse it with the exact same payload when retrying."
          }
        ],
        "responses": {
          "201": {
            "description": "Requires create. Creates a public customer request as the key\u2019s contact. Returns 201 and an id, including on an identical retry. Customer identity comes from the key, not the payload."
          },
          "400": {
            "description": "Malformed request. Fix the request before retrying."
          },
          "401": {
            "description": "The key is invalid, expired, revoked or its sign-in or company access changed. Sign in to the portal and replace it."
          },
          "403": {
            "description": "The key lacks the required scope, the customer role does not allow this action, or portal terms must be accepted."
          },
          "404": {
            "description": "This ticket is not available to this customer in this portal."
          },
          "409": {
            "description": "This Idempotency-Key already belongs to a different operation or payload. Check your saved request."
          },
          "422": {
            "description": "Invalid fields, UUID, page, or missing Idempotency-Key. Correct the request."
          },
          "429": {
            "description": "Too many requests. Wait at least a minute and retry with backoff."
          },
          "503": {
            "description": "Support is temporarily unavailable. Retry with backoff and the same idempotency key for writes."
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewTicket"
              },
              "example": {
                "subject": "API connection issue",
                "body": "## What happened\nThe nightly job returned an error.\n\n```text\nrequest_id: demo-42\nstatus: 503\n```",
                "format": "markdown"
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "JSON",
            "label": "ticket.json",
            "source": "{\n  \"subject\": \"API connection issue\",\n  \"body\": \"## What happened\\nThe nightly job returned an error.\\n\\n```text\\nrequest_id: demo-42\\nstatus: 503\\n```\",\n  \"format\": \"markdown\"\n}"
          },
          {
            "lang": "Shell",
            "label": "cURL (POSIX shell)",
            "source": "curl --fail-with-body --silent --show-error --request POST \\\n  \"$ESCALIO_PORTAL/api/portal/customer/v1/tickets\" \\\n  --header \"Authorization: Bearer $ESCALIO_CUSTOMER_KEY\" \\\n  --header \"Content-Type: application/json\" \\\n  --header \"Idempotency-Key: $ESCALIO_REQUEST_ID\" \\\n  --data-binary @ticket.json"
          },
          {
            "lang": "Python",
            "label": "Python 3",
            "source": "import json, os\nfrom urllib.request import Request, urlopen\nfrom urllib.error import HTTPError\n\npayload = open(\"ticket.json\", \"rb\").read()\nrequest = Request(os.environ[\"ESCALIO_PORTAL\"].rstrip(\"/\") + \"/api/portal/customer/v1/tickets\",\n    method=\"POST\", data=payload, headers={\n        \"Authorization\": \"Bearer \" + os.environ[\"ESCALIO_CUSTOMER_KEY\"],\n        \"Content-Type\": \"application/json\",\n        \"Idempotency-Key\": os.environ[\"ESCALIO_REQUEST_ID\"],\n    })\ntry:\n    with urlopen(request, timeout=30) as response:\n        print(json.loads(response.read()))\nexcept HTTPError as error:\n    raise SystemExit(f\"HTTP {error.code}: {error.read().decode()}\")\n"
          }
        ]
      }
    },
    "/api/portal/customer/v1/tickets/{ticket_id}": {
      "get": {
        "operationId": "ticket",
        "summary": "Read a request",
        "description": "Requires read. Public messages only, oldest first. Returns ticket, messages, page, page_size (100), total, follow_up_of and follow_ups. Each message includes body, format, author, own, created_at and attachment metadata. Internal notes are never returned.",
        "parameters": [
          {
            "name": "ticket_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Required ticket UUID returned by a create or list operation."
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000000,
              "default": 1
            },
            "description": "Optional page number, 1\u20131,000,000; defaults to 1. Use ?page=2 to read the next page."
          }
        ],
        "responses": {
          "200": {
            "description": "Requires read. Public messages only, oldest first. Returns ticket, messages, page, page_size (100), total, follow_up_of and follow_ups. Each message includes body, format, author, own, created_at and attachment metadata. Internal notes are never returned."
          },
          "400": {
            "description": "Malformed request. Fix the request before retrying."
          },
          "401": {
            "description": "The key is invalid, expired, revoked or its sign-in or company access changed. Sign in to the portal and replace it."
          },
          "403": {
            "description": "The key lacks the required scope, the customer role does not allow this action, or portal terms must be accepted."
          },
          "404": {
            "description": "This ticket is not available to this customer in this portal."
          },
          "409": {
            "description": "This Idempotency-Key already belongs to a different operation or payload. Check your saved request."
          },
          "422": {
            "description": "Invalid fields, UUID, page, or missing Idempotency-Key. Correct the request."
          },
          "429": {
            "description": "Too many requests. Wait at least a minute and retry with backoff."
          },
          "503": {
            "description": "Support is temporarily unavailable. Retry with backoff and the same idempotency key for writes."
          }
        },
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "cURL (POSIX shell)",
            "source": "curl --fail-with-body --silent --show-error --request GET \\\n  \"$ESCALIO_PORTAL/api/portal/customer/v1/tickets/$ESCALIO_TICKET_ID\" \\\n  --header \"Authorization: Bearer $ESCALIO_CUSTOMER_KEY\""
          }
        ]
      }
    },
    "/api/portal/customer/v1/tickets/{ticket_id}/messages": {
      "post": {
        "operationId": "reply",
        "summary": "Reply to a request",
        "description": "Requires reply. Sends a public reply. A closed request may create a follow-up: use the returned id, which can differ from ticket_id. Identical retries return 201 with the same id.",
        "parameters": [
          {
            "name": "ticket_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Required ticket UUID returned by a create or list operation."
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 8,
              "maxLength": 100
            },
            "description": "Persist a unique ID for this operation; reuse it with the exact same payload when retrying."
          }
        ],
        "responses": {
          "201": {
            "description": "Requires reply. Sends a public reply. A closed request may create a follow-up: use the returned id, which can differ from ticket_id. Identical retries return 201 with the same id."
          },
          "400": {
            "description": "Malformed request. Fix the request before retrying."
          },
          "401": {
            "description": "The key is invalid, expired, revoked or its sign-in or company access changed. Sign in to the portal and replace it."
          },
          "403": {
            "description": "The key lacks the required scope, the customer role does not allow this action, or portal terms must be accepted."
          },
          "404": {
            "description": "This ticket is not available to this customer in this portal."
          },
          "409": {
            "description": "This Idempotency-Key already belongs to a different operation or payload. Check your saved request."
          },
          "422": {
            "description": "Invalid fields, UUID, page, or missing Idempotency-Key. Correct the request."
          },
          "429": {
            "description": "Too many requests. Wait at least a minute and retry with backoff."
          },
          "503": {
            "description": "Support is temporarily unavailable. Retry with backoff and the same idempotency key for writes."
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Message"
              },
              "example": {
                "body": "The issue is resolved, thank you.",
                "format": "plain"
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "JSON",
            "label": "reply.json",
            "source": "{\n  \"body\": \"The issue is resolved, thank you.\",\n  \"format\": \"plain\"\n}"
          },
          {
            "lang": "Shell",
            "label": "cURL (POSIX shell)",
            "source": "curl --fail-with-body --silent --show-error --request POST \\\n  \"$ESCALIO_PORTAL/api/portal/customer/v1/tickets/$ESCALIO_TICKET_ID/messages\" \\\n  --header \"Authorization: Bearer $ESCALIO_CUSTOMER_KEY\" \\\n  --header \"Content-Type: application/json\" \\\n  --header \"Idempotency-Key: $ESCALIO_REQUEST_ID\" \\\n  --data-binary @reply.json"
          }
        ]
      }
    },
    "/api/portal/customer/v1/logout": {
      "post": {
        "operationId": "logout",
        "summary": "Revoke this key",
        "description": "No ticket scope is required. Immediately revokes this credential. Returns {\"revoked\": true}; further calls with this key return 401.",
        "parameters": [],
        "responses": {
          "200": {
            "description": "No ticket scope is required. Immediately revokes this credential. Returns {\"revoked\": true}; further calls with this key return 401."
          },
          "400": {
            "description": "Malformed request. Fix the request before retrying."
          },
          "401": {
            "description": "The key is invalid, expired, revoked or its sign-in or company access changed. Sign in to the portal and replace it."
          },
          "403": {
            "description": "The key lacks the required scope, the customer role does not allow this action, or portal terms must be accepted."
          },
          "404": {
            "description": "This ticket is not available to this customer in this portal."
          },
          "409": {
            "description": "This Idempotency-Key already belongs to a different operation or payload. Check your saved request."
          },
          "422": {
            "description": "Invalid fields, UUID, page, or missing Idempotency-Key. Correct the request."
          },
          "429": {
            "description": "Too many requests. Wait at least a minute and retry with backoff."
          },
          "503": {
            "description": "Support is temporarily unavailable. Retry with backoff and the same idempotency key for writes."
          }
        },
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "cURL (POSIX shell)",
            "source": "curl --fail-with-body --silent --show-error --request POST \\\n  \"$ESCALIO_PORTAL/api/portal/customer/v1/logout\" \\\n  --header \"Authorization: Bearer $ESCALIO_CUSTOMER_KEY\""
          }
        ]
      }
    }
  }
}
