{
  "openapi": "3.1.0",
  "info": {
    "title": "Alty public API",
    "version": "1",
    "description": "The machine API of an Alty instance. Every instance has its own host: `https://<slug>.alty.one`. Each route names the key scope it needs (`x-alty-scope`) and the module that must be switched on (`x-alty-module`). Start with `GET /api/public/v1/whoami` to check a key. Every response carries an `x-request-id`; quote it when asking Alty support about a request."
  },
  "servers": [
    {
      "url": "https://{slug}.alty.one",
      "variables": {
        "slug": {
          "default": "your-business",
          "description": "Your instance's slug."
        }
      }
    }
  ],
  "components": {
    "securitySchemes": {
      "altyKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "An Alty API key (`altyk_…`) created in Admin › API on the instance. Send `Authorization: Bearer <key>` (or `x-api-key: <key>`). Server-side only — never in browser code."
      }
    },
    "schemas": {
      "Whoami": {
        "type": "object",
        "properties": {
          "tenant": {
            "type": "string",
            "description": "The instance's slug: your API lives at https://<slug>.alty.one."
          },
          "key": {
            "type": "object",
            "properties": {
              "label": {
                "type": "string"
              },
              "scopes": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "What this key may call. Each endpoint names the scope it needs."
              },
              "expires_at": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "Epoch milliseconds, or null for a key that never expires."
              }
            },
            "required": [
              "label",
              "scopes",
              "expires_at"
            ]
          },
          "service_account": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              }
            },
            "required": [
              "id",
              "name"
            ],
            "description": "The member this key's changes are credited to, if it has one."
          },
          "modules": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Which of the modules this key's scopes belong to are switched on for this instance. Only those modules are listed."
          }
        },
        "required": [
          "tenant",
          "key",
          "service_account",
          "modules"
        ]
      },
      "Unauthorized": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "unauthorized"
            ]
          }
        },
        "required": [
          "error"
        ]
      },
      "KeyRateLimited": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "rate_limited"
            ]
          }
        },
        "required": [
          "error"
        ]
      },
      "CustomerPage": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "anyOf": [
                {
                  "$ref": "#/components/schemas/Customer"
                },
                {
                  "$ref": "#/components/schemas/Tombstone"
                }
              ]
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Pass as `cursor` for the next page. Null means you have everything, as of now."
          }
        },
        "required": [
          "data",
          "next_cursor"
        ]
      },
      "Customer": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "description": "ISO-8601 UTC, e.g. 2026-10-22T03:04:05.000Z."
          },
          "updated_at": {
            "type": "string",
            "description": "ISO-8601 UTC, e.g. 2026-10-22T03:04:05.000Z."
          },
          "version": {
            "type": "integer",
            "description": "The record’s last change, in epoch milliseconds. Send it back as `If-Match` on a write to refuse overwriting a newer edit."
          },
          "name": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "description": "A CUSTOMER_TYPE picklist key, e.g. CUSTOMER."
          },
          "archived": {
            "type": "boolean"
          },
          "contact_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "The primary contact, for list display. All contacts: GET /customers/{id}/contacts."
          },
          "email": {
            "type": [
              "string",
              "null"
            ]
          },
          "phone": {
            "type": [
              "string",
              "null"
            ]
          },
          "mobile": {
            "type": [
              "string",
              "null"
            ]
          },
          "address": {
            "$ref": "#/components/schemas/Address"
          },
          "abn": {
            "type": [
              "string",
              "null"
            ]
          },
          "website": {
            "type": [
              "string",
              "null"
            ]
          },
          "notes": {
            "type": [
              "string",
              "null"
            ]
          },
          "custom_fields": {
            "type": "object",
            "additionalProperties": {},
            "description": "This instance’s own fields for the record, as an object."
          },
          "external_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "YOUR id for this record, if your integration set one. Only your service account’s ids are ever returned; null for a key without a service account."
          }
        },
        "required": [
          "id",
          "created_at",
          "updated_at",
          "version",
          "name",
          "type",
          "archived",
          "contact_name",
          "email",
          "phone",
          "mobile",
          "address",
          "abn",
          "website",
          "notes",
          "custom_fields",
          "external_id"
        ]
      },
      "Address": {
        "type": "object",
        "properties": {
          "line1": {
            "type": [
              "string",
              "null"
            ]
          },
          "line2": {
            "type": [
              "string",
              "null"
            ]
          },
          "suburb": {
            "type": [
              "string",
              "null"
            ]
          },
          "state": {
            "type": [
              "string",
              "null"
            ]
          },
          "postcode": {
            "type": [
              "string",
              "null"
            ]
          },
          "country": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "line1",
          "line2",
          "suburb",
          "state",
          "postcode",
          "country"
        ]
      },
      "Tombstone": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "deleted_at": {
            "type": "string",
            "description": "When it went to the Trash. A row carrying `deleted_at` is a tombstone: delete your copy."
          },
          "updated_at": {
            "type": "string",
            "description": "ISO-8601 UTC, e.g. 2026-10-22T03:04:05.000Z."
          },
          "version": {
            "type": "integer",
            "description": "The record’s last change, in epoch milliseconds. Send it back as `If-Match` on a write to refuse overwriting a newer edit."
          }
        },
        "required": [
          "id",
          "deleted_at",
          "updated_at",
          "version"
        ]
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          }
        },
        "required": [
          "error"
        ]
      },
      "KeyForbidden": {
        "anyOf": [
          {
            "type": "object",
            "properties": {
              "error": {
                "type": "string",
                "enum": [
                  "missing_scope"
                ]
              },
              "needs": {
                "type": "string",
                "description": "The scope this endpoint needs. Ask your Alty admin for a key that has it."
              }
            },
            "required": [
              "error",
              "needs"
            ]
          },
          {
            "type": "object",
            "properties": {
              "error": {
                "type": "string",
                "enum": [
                  "module_off"
                ]
              },
              "module": {
                "type": "string",
                "description": "The module that is switched off for this instance, e.g. `support`."
              }
            },
            "required": [
              "error",
              "module"
            ]
          }
        ]
      },
      "LeadPage": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "anyOf": [
                {
                  "$ref": "#/components/schemas/Lead"
                },
                {
                  "$ref": "#/components/schemas/Tombstone"
                }
              ]
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Pass as `cursor` for the next page. Null means you have everything, as of now."
          }
        },
        "required": [
          "data",
          "next_cursor"
        ]
      },
      "Lead": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "description": "ISO-8601 UTC, e.g. 2026-10-22T03:04:05.000Z."
          },
          "updated_at": {
            "type": "string",
            "description": "ISO-8601 UTC, e.g. 2026-10-22T03:04:05.000Z."
          },
          "version": {
            "type": "integer",
            "description": "The record’s last change, in epoch milliseconds. Send it back as `If-Match` on a write to refuse overwriting a newer edit."
          },
          "number": {
            "type": "string",
            "description": "The human reference, e.g. ENQ-0042."
          },
          "subject": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "description": "An ENQUIRY_STATUS picklist key."
          },
          "source": {
            "type": "string",
            "description": "An ENQUIRY_SOURCE picklist key."
          },
          "service_type": {
            "type": [
              "string",
              "null"
            ],
            "description": "A SERVICE_TYPE picklist key."
          },
          "referred_by": {
            "type": [
              "string",
              "null"
            ]
          },
          "customer_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Set once the lead has a customer (or was converted)."
          },
          "contact": {
            "type": "object",
            "properties": {
              "name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "company": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "email": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "phone": {
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "required": [
              "name",
              "company",
              "email",
              "phone"
            ]
          },
          "assigned_to": {
            "type": [
              "string",
              "null"
            ],
            "description": "A member id (GET /members)."
          },
          "notes": {
            "type": [
              "string",
              "null"
            ]
          },
          "converted_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO-8601 UTC, e.g. 2026-10-22T03:04:05.000Z."
          },
          "custom_fields": {
            "type": "object",
            "additionalProperties": {},
            "description": "This instance’s own fields for the record, as an object."
          },
          "external_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "YOUR id for this record, if your integration set one. Only your service account’s ids are ever returned; null for a key without a service account."
          }
        },
        "required": [
          "id",
          "created_at",
          "updated_at",
          "version",
          "number",
          "subject",
          "status",
          "source",
          "service_type",
          "referred_by",
          "customer_id",
          "contact",
          "assigned_to",
          "notes",
          "converted_at",
          "custom_fields",
          "external_id"
        ]
      },
      "ProjectPage": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "anyOf": [
                {
                  "$ref": "#/components/schemas/Project"
                },
                {
                  "$ref": "#/components/schemas/Tombstone"
                }
              ]
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Pass as `cursor` for the next page. Null means you have everything, as of now."
          }
        },
        "required": [
          "data",
          "next_cursor"
        ]
      },
      "Project": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "description": "ISO-8601 UTC, e.g. 2026-10-22T03:04:05.000Z."
          },
          "updated_at": {
            "type": "string",
            "description": "ISO-8601 UTC, e.g. 2026-10-22T03:04:05.000Z."
          },
          "version": {
            "type": "integer",
            "description": "The record’s last change, in epoch milliseconds. Send it back as `If-Match` on a write to refuse overwriting a newer edit."
          },
          "number": {
            "type": "string",
            "description": "The human reference, e.g. PRJ-0007."
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "customer_id": {
            "type": "string"
          },
          "site_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "lead_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "The lead it was converted from, if any."
          },
          "service_type": {
            "type": "string",
            "description": "A SERVICE_TYPE picklist key."
          },
          "status": {
            "type": "string",
            "description": "A PROJECT_STATUS picklist key."
          },
          "priority": {
            "type": [
              "string",
              "null"
            ],
            "description": "A PROJECT_PRIORITY picklist key."
          },
          "assigned_to": {
            "type": [
              "string",
              "null"
            ],
            "description": "A member id (GET /members)."
          },
          "start_date": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO-8601 UTC, e.g. 2026-10-22T03:04:05.000Z."
          },
          "due_date": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO-8601 UTC, e.g. 2026-10-22T03:04:05.000Z."
          },
          "completed_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO-8601 UTC, e.g. 2026-10-22T03:04:05.000Z."
          },
          "estimated_value": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Integer minor units (cents)."
          },
          "custom_fields": {
            "type": "object",
            "additionalProperties": {},
            "description": "This instance’s own fields for the record, as an object."
          },
          "external_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "YOUR id for this record, if your integration set one. Only your service account’s ids are ever returned; null for a key without a service account."
          }
        },
        "required": [
          "id",
          "created_at",
          "updated_at",
          "version",
          "number",
          "title",
          "description",
          "customer_id",
          "site_id",
          "lead_id",
          "service_type",
          "status",
          "priority",
          "assigned_to",
          "start_date",
          "due_date",
          "completed_at",
          "estimated_value",
          "custom_fields",
          "external_id"
        ]
      },
      "QuotePage": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "anyOf": [
                {
                  "$ref": "#/components/schemas/Quote"
                },
                {
                  "$ref": "#/components/schemas/Tombstone"
                }
              ]
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Pass as `cursor` for the next page. Null means you have everything, as of now."
          }
        },
        "required": [
          "data",
          "next_cursor"
        ]
      },
      "Quote": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "description": "ISO-8601 UTC, e.g. 2026-10-22T03:04:05.000Z."
          },
          "updated_at": {
            "type": "string",
            "description": "ISO-8601 UTC, e.g. 2026-10-22T03:04:05.000Z."
          },
          "version": {
            "type": "integer",
            "description": "The record’s last change, in epoch milliseconds. Send it back as `If-Match` on a write to refuse overwriting a newer edit."
          },
          "number": {
            "type": "string"
          },
          "revision": {
            "type": "integer",
            "description": "1 for the first; a revised proposal is a new quote with revision_of set."
          },
          "revision_of": {
            "type": [
              "string",
              "null"
            ]
          },
          "customer_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Null for a proposal to a lead that has no customer yet (lead_id is then set)."
          },
          "lead_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "project_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": "string",
            "description": "A QUOTE_STATUS picklist key."
          },
          "currency": {
            "type": "string",
            "description": "ISO 4217, e.g. AUD."
          },
          "tax_rate": {
            "type": "number",
            "description": "A fraction: 0.1 is 10%."
          },
          "amounts": {
            "type": "string",
            "enum": [
              "EXCLUSIVE",
              "INCLUSIVE",
              "NOTAX"
            ],
            "description": "Whether line amounts are before tax (EXCLUSIVE), include it (INCLUSIVE), or bear none (NOTAX)."
          },
          "subtotal": {
            "type": "integer",
            "description": "Integer minor units (cents)."
          },
          "tax_amount": {
            "type": "integer",
            "description": "Integer minor units (cents)."
          },
          "total": {
            "type": "integer",
            "description": "Integer minor units (cents)."
          },
          "valid_until": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO-8601 UTC, e.g. 2026-10-22T03:04:05.000Z."
          },
          "expires_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO-8601 UTC, e.g. 2026-10-22T03:04:05.000Z."
          },
          "sent_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO-8601 UTC, e.g. 2026-10-22T03:04:05.000Z."
          },
          "decided_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO-8601 UTC, e.g. 2026-10-22T03:04:05.000Z."
          },
          "decline_reason": {
            "type": [
              "string",
              "null"
            ]
          },
          "decline_detail": {
            "type": [
              "string",
              "null"
            ]
          },
          "converted_invoice_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "converted_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO-8601 UTC, e.g. 2026-10-22T03:04:05.000Z."
          },
          "notes": {
            "type": [
              "string",
              "null"
            ]
          },
          "custom_fields": {
            "type": "object",
            "additionalProperties": {},
            "description": "This instance’s own fields for the record, as an object."
          },
          "lines": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LineItem"
            },
            "description": "On GET /quotes/{id} only."
          }
        },
        "required": [
          "id",
          "created_at",
          "updated_at",
          "version",
          "number",
          "revision",
          "revision_of",
          "customer_id",
          "lead_id",
          "project_id",
          "status",
          "currency",
          "tax_rate",
          "amounts",
          "subtotal",
          "tax_amount",
          "total",
          "valid_until",
          "expires_at",
          "sent_at",
          "decided_at",
          "decline_reason",
          "decline_detail",
          "converted_invoice_id",
          "converted_at",
          "notes",
          "custom_fields"
        ]
      },
      "LineItem": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "quantity": {
            "type": "number"
          },
          "unit_price": {
            "type": "integer",
            "description": "Integer minor units (cents)."
          },
          "discount_bp": {
            "type": "integer",
            "description": "Line discount in basis points: 1250 is 12.5%, 10000 is free."
          },
          "amount": {
            "type": "integer",
            "description": "Integer minor units (cents), net of the discount."
          },
          "item_code": {
            "type": [
              "string",
              "null"
            ]
          },
          "account_code": {
            "type": [
              "string",
              "null"
            ]
          },
          "tax_code": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "id",
          "description",
          "quantity",
          "unit_price",
          "discount_bp",
          "amount",
          "item_code",
          "account_code",
          "tax_code"
        ]
      },
      "InvoicePage": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "anyOf": [
                {
                  "$ref": "#/components/schemas/Invoice"
                },
                {
                  "$ref": "#/components/schemas/Tombstone"
                }
              ]
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Pass as `cursor` for the next page. Null means you have everything, as of now."
          }
        },
        "required": [
          "data",
          "next_cursor"
        ]
      },
      "Invoice": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "description": "ISO-8601 UTC, e.g. 2026-10-22T03:04:05.000Z."
          },
          "updated_at": {
            "type": "string",
            "description": "ISO-8601 UTC, e.g. 2026-10-22T03:04:05.000Z."
          },
          "version": {
            "type": "integer",
            "description": "The record’s last change, in epoch milliseconds. Send it back as `If-Match` on a write to refuse overwriting a newer edit."
          },
          "number": {
            "type": "string"
          },
          "customer_id": {
            "type": "string"
          },
          "project_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": "string",
            "description": "An INVOICE_STATUS picklist key, as the app shows it (Xero or Books keeps it current where connected)."
          },
          "is_overdue": {
            "type": "boolean",
            "description": "Computed at read time from due_at, the app’s own rule. It can turn true without the invoice changing, so it does not move `updated_at`."
          },
          "currency": {
            "type": "string",
            "description": "ISO 4217, e.g. AUD."
          },
          "tax_rate": {
            "type": "number",
            "description": "A fraction: 0.1 is 10%."
          },
          "amounts": {
            "type": "string",
            "enum": [
              "EXCLUSIVE",
              "INCLUSIVE",
              "NOTAX"
            ],
            "description": "Whether line amounts are before tax (EXCLUSIVE), include it (INCLUSIVE), or bear none (NOTAX)."
          },
          "subtotal": {
            "type": "integer",
            "description": "Integer minor units (cents)."
          },
          "tax_amount": {
            "type": "integer",
            "description": "Integer minor units (cents)."
          },
          "total": {
            "type": "integer",
            "description": "Integer minor units (cents)."
          },
          "amount_due": {
            "type": [
              "integer",
              "null"
            ],
            "description": "What is still owed, in cents. Null means unknown (not tracked for this invoice) — never read it as zero."
          },
          "issued_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO-8601 UTC, e.g. 2026-10-22T03:04:05.000Z."
          },
          "due_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO-8601 UTC, e.g. 2026-10-22T03:04:05.000Z."
          },
          "sent_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO-8601 UTC, e.g. 2026-10-22T03:04:05.000Z."
          },
          "paid_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO-8601 UTC, e.g. 2026-10-22T03:04:05.000Z."
          },
          "notes": {
            "type": [
              "string",
              "null"
            ]
          },
          "custom_fields": {
            "type": "object",
            "additionalProperties": {},
            "description": "This instance’s own fields for the record, as an object."
          },
          "lines": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LineItem"
            },
            "description": "On GET /invoices/{id} only."
          }
        },
        "required": [
          "id",
          "created_at",
          "updated_at",
          "version",
          "number",
          "customer_id",
          "project_id",
          "status",
          "is_overdue",
          "currency",
          "tax_rate",
          "amounts",
          "subtotal",
          "tax_amount",
          "total",
          "amount_due",
          "issued_at",
          "due_at",
          "sent_at",
          "paid_at",
          "notes",
          "custom_fields"
        ]
      },
      "SitePage": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "anyOf": [
                {
                  "$ref": "#/components/schemas/Site"
                },
                {
                  "$ref": "#/components/schemas/Tombstone"
                }
              ]
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Pass as `cursor` for the next page. Null means you have everything, as of now."
          }
        },
        "required": [
          "data",
          "next_cursor"
        ]
      },
      "Site": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "description": "ISO-8601 UTC, e.g. 2026-10-22T03:04:05.000Z."
          },
          "updated_at": {
            "type": "string",
            "description": "ISO-8601 UTC, e.g. 2026-10-22T03:04:05.000Z."
          },
          "version": {
            "type": "integer",
            "description": "The record’s last change, in epoch milliseconds. Send it back as `If-Match` on a write to refuse overwriting a newer edit."
          },
          "customer_id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "address": {
            "$ref": "#/components/schemas/Address"
          },
          "contact_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "contact_phone": {
            "type": [
              "string",
              "null"
            ]
          },
          "access_notes": {
            "type": [
              "string",
              "null"
            ]
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "custom_fields": {
            "type": "object",
            "additionalProperties": {},
            "description": "This instance’s own fields for the record, as an object."
          },
          "external_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "YOUR id for this record, if your integration set one. Only your service account’s ids are ever returned; null for a key without a service account."
          }
        },
        "required": [
          "id",
          "created_at",
          "updated_at",
          "version",
          "customer_id",
          "name",
          "address",
          "contact_name",
          "contact_phone",
          "access_notes",
          "description",
          "custom_fields",
          "external_id"
        ]
      },
      "ContactPage": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Contact"
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Pass as `cursor` for the next page. Null means you have everything, as of now."
          }
        },
        "required": [
          "data",
          "next_cursor"
        ]
      },
      "Contact": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "description": "ISO-8601 UTC, e.g. 2026-10-22T03:04:05.000Z."
          },
          "updated_at": {
            "type": "string",
            "description": "ISO-8601 UTC, e.g. 2026-10-22T03:04:05.000Z."
          },
          "version": {
            "type": "integer",
            "description": "The record’s last change, in epoch milliseconds. Send it back as `If-Match` on a write to refuse overwriting a newer edit."
          },
          "customer_id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "email": {
            "type": [
              "string",
              "null"
            ]
          },
          "phone": {
            "type": [
              "string",
              "null"
            ]
          },
          "role": {
            "type": [
              "string",
              "null"
            ]
          },
          "is_primary": {
            "type": "boolean"
          },
          "custom_fields": {
            "type": "object",
            "additionalProperties": {},
            "description": "This instance’s own fields for the record, as an object."
          }
        },
        "required": [
          "id",
          "created_at",
          "updated_at",
          "version",
          "customer_id",
          "name",
          "email",
          "phone",
          "role",
          "is_primary",
          "custom_fields"
        ]
      },
      "NotePage": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "anyOf": [
                {
                  "$ref": "#/components/schemas/Note"
                },
                {
                  "$ref": "#/components/schemas/Tombstone"
                }
              ]
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Pass as `cursor` for the next page. Null means you have everything, as of now."
          }
        },
        "required": [
          "data",
          "next_cursor"
        ]
      },
      "Note": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "description": "ISO-8601 UTC, e.g. 2026-10-22T03:04:05.000Z."
          },
          "updated_at": {
            "type": "string",
            "description": "ISO-8601 UTC, e.g. 2026-10-22T03:04:05.000Z."
          },
          "version": {
            "type": "integer",
            "description": "The record’s last change, in epoch milliseconds. Send it back as `If-Match` on a write to refuse overwriting a newer edit."
          },
          "parent": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "customer",
                  "lead",
                  "project"
                ]
              },
              "id": {
                "type": "string"
              }
            },
            "required": [
              "type",
              "id"
            ]
          },
          "type": {
            "type": "string",
            "description": "A NOTE_TYPE picklist key, e.g. GENERAL."
          },
          "body": {
            "type": "string"
          },
          "direction": {
            "type": [
              "string",
              "null"
            ],
            "description": "For a note that is a message on a lead’s thread: INBOUND or OUTBOUND."
          },
          "author": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "kind": {
                "type": "string",
                "enum": [
                  "person",
                  "service"
                ],
                "description": "`service` is an integration’s service account."
              }
            },
            "required": [
              "id",
              "name",
              "kind"
            ]
          },
          "edited_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO-8601 UTC, e.g. 2026-10-22T03:04:05.000Z."
          }
        },
        "required": [
          "id",
          "created_at",
          "updated_at",
          "version",
          "parent",
          "type",
          "body",
          "direction",
          "author",
          "edited_at"
        ]
      },
      "MemberPage": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Member"
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Pass as `cursor` for the next page. Null means you have everything, as of now."
          }
        },
        "required": [
          "data",
          "next_cursor"
        ]
      },
      "Member": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "What `assigned_to` holds."
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "email": {
            "type": [
              "string",
              "null"
            ]
          },
          "role": {
            "type": "string",
            "enum": [
              "admin",
              "staff"
            ]
          },
          "kind": {
            "type": "string",
            "enum": [
              "person",
              "service"
            ]
          },
          "active": {
            "type": "boolean",
            "description": "False for a deactivated member: still named on old records, but not assignable."
          }
        },
        "required": [
          "id",
          "name",
          "email",
          "role",
          "kind",
          "active"
        ]
      },
      "InvalidIdempotencyKey": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "invalid_idempotency_key"
            ]
          }
        },
        "required": [
          "error"
        ]
      },
      "IdempotencyConflict": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "idempotency_conflict",
              "idempotency_in_progress"
            ]
          }
        },
        "required": [
          "error"
        ]
      },
      "CustomerCreate": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 10000
          },
          "type": {
            "type": "string",
            "description": "A CUSTOMER_TYPE picklist key. Absent on create: the instance default."
          },
          "archived": {
            "type": "boolean",
            "description": "Archive or unarchive (the app’s Archive). Not a delete."
          },
          "contact_name": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 10000
          },
          "email": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 10000
          },
          "phone": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 10000
          },
          "mobile": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 10000
          },
          "address": {
            "type": "object",
            "properties": {
              "line1": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 10000
              },
              "line2": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 10000
              },
              "suburb": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 10000
              },
              "state": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 10000
              },
              "postcode": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 10000
              },
              "country": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 10000
              }
            },
            "additionalProperties": false,
            "description": "Send only the lines you change; a line sent as null is cleared."
          },
          "abn": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 10000
          },
          "website": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 10000
          },
          "notes": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 10000
          },
          "custom_fields": {
            "type": "object",
            "additionalProperties": {},
            "description": "This instance’s own fields, as an object. REPLACES the whole object: read it, change it, send it all back."
          },
          "external_id": {
            "type": [
              "string",
              "null"
            ],
            "minLength": 1,
            "maxLength": 200,
            "description": "YOUR id for this record. On POST, a record you already gave this id is UPDATED instead (200, not 201). On PATCH, sets it; null removes it. Kept per service account: another integration never sees it."
          }
        },
        "required": [
          "name"
        ],
        "additionalProperties": false
      },
      "VersionConflict": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "version_conflict"
            ]
          },
          "current": {
            "type": "integer",
            "description": "The record’s version now. Read it again, merge, and retry with this."
          }
        },
        "required": [
          "error",
          "current"
        ]
      },
      "CustomerPatch": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 10000
          },
          "type": {
            "type": "string",
            "description": "A CUSTOMER_TYPE picklist key. Absent on create: the instance default."
          },
          "archived": {
            "type": "boolean",
            "description": "Archive or unarchive (the app’s Archive). Not a delete."
          },
          "contact_name": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 10000
          },
          "email": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 10000
          },
          "phone": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 10000
          },
          "mobile": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 10000
          },
          "address": {
            "type": "object",
            "properties": {
              "line1": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 10000
              },
              "line2": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 10000
              },
              "suburb": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 10000
              },
              "state": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 10000
              },
              "postcode": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 10000
              },
              "country": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 10000
              }
            },
            "additionalProperties": false,
            "description": "Send only the lines you change; a line sent as null is cleared."
          },
          "abn": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 10000
          },
          "website": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 10000
          },
          "notes": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 10000
          },
          "custom_fields": {
            "type": "object",
            "additionalProperties": {},
            "description": "This instance’s own fields, as an object. REPLACES the whole object: read it, change it, send it all back."
          },
          "external_id": {
            "type": [
              "string",
              "null"
            ],
            "minLength": 1,
            "maxLength": 200,
            "description": "YOUR id for this record. On POST, a record you already gave this id is UPDATED instead (200, not 201). On PATCH, sets it; null removes it. Kept per service account: another integration never sees it."
          }
        },
        "additionalProperties": false
      },
      "Deleted": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "deleted": {
            "type": "boolean",
            "enum": [
              true
            ]
          }
        },
        "required": [
          "id",
          "deleted"
        ]
      },
      "LeadCreate": {
        "type": "object",
        "properties": {
          "subject": {
            "type": "string",
            "minLength": 1,
            "maxLength": 10000
          },
          "status": {
            "type": "string",
            "description": "An ENQUIRY_STATUS picklist key. A move is recorded in the lead’s History, credited to your service account."
          },
          "source": {
            "type": "string",
            "description": "An ENQUIRY_SOURCE picklist key."
          },
          "service_type": {
            "type": [
              "string",
              "null"
            ],
            "description": "A SERVICE_TYPE picklist key."
          },
          "referred_by": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 10000
          },
          "contact": {
            "type": "object",
            "properties": {
              "name": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 10000
              },
              "company": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 10000
              },
              "email": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 10000
              },
              "phone": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 10000
              }
            },
            "additionalProperties": false,
            "description": "Who the lead is from. Send only what you change."
          },
          "assigned_to": {
            "type": [
              "string",
              "null"
            ],
            "description": "A member id from GET /members, or null for nobody. A deactivated member is refused."
          },
          "notes": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 10000
          },
          "custom_fields": {
            "type": "object",
            "additionalProperties": {},
            "description": "This instance’s own fields, as an object. REPLACES the whole object: read it, change it, send it all back."
          },
          "external_id": {
            "type": [
              "string",
              "null"
            ],
            "minLength": 1,
            "maxLength": 200,
            "description": "YOUR id for this record. On POST, a record you already gave this id is UPDATED instead (200, not 201). On PATCH, sets it; null removes it. Kept per service account: another integration never sees it."
          }
        },
        "required": [
          "subject"
        ],
        "additionalProperties": false
      },
      "LeadPatch": {
        "type": "object",
        "properties": {
          "subject": {
            "type": "string",
            "minLength": 1,
            "maxLength": 10000
          },
          "status": {
            "type": "string",
            "description": "An ENQUIRY_STATUS picklist key. A move is recorded in the lead’s History, credited to your service account."
          },
          "source": {
            "type": "string",
            "description": "An ENQUIRY_SOURCE picklist key."
          },
          "service_type": {
            "type": [
              "string",
              "null"
            ],
            "description": "A SERVICE_TYPE picklist key."
          },
          "referred_by": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 10000
          },
          "contact": {
            "type": "object",
            "properties": {
              "name": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 10000
              },
              "company": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 10000
              },
              "email": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 10000
              },
              "phone": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 10000
              }
            },
            "additionalProperties": false,
            "description": "Who the lead is from. Send only what you change."
          },
          "assigned_to": {
            "type": [
              "string",
              "null"
            ],
            "description": "A member id from GET /members, or null for nobody. A deactivated member is refused."
          },
          "notes": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 10000
          },
          "custom_fields": {
            "type": "object",
            "additionalProperties": {},
            "description": "This instance’s own fields, as an object. REPLACES the whole object: read it, change it, send it all back."
          },
          "external_id": {
            "type": [
              "string",
              "null"
            ],
            "minLength": 1,
            "maxLength": 200,
            "description": "YOUR id for this record. On POST, a record you already gave this id is UPDATED instead (200, not 201). On PATCH, sets it; null removes it. Kept per service account: another integration never sees it."
          }
        },
        "additionalProperties": false
      },
      "ProjectCreate": {
        "type": "object",
        "properties": {
          "title": {
            "type": "string",
            "minLength": 1,
            "maxLength": 10000
          },
          "customer_id": {
            "type": "string",
            "minLength": 1,
            "description": "The customer the project is for."
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 10000
          },
          "site_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "One of the customer’s sites, or null."
          },
          "service_type": {
            "type": "string",
            "description": "A SERVICE_TYPE picklist key."
          },
          "status": {
            "type": "string",
            "description": "A PROJECT_STATUS picklist key, checked against the instance’s list exactly as the app checks it. A move is recorded in the project’s History, credited to your service account."
          },
          "status_reason": {
            "type": "string",
            "maxLength": 500,
            "description": "Why the status moved, for the History. Only with `status`."
          },
          "priority": {
            "type": "string",
            "description": "A PROJECT_PRIORITY picklist key."
          },
          "assigned_to": {
            "type": [
              "string",
              "null"
            ],
            "description": "A member id from GET /members, or null for nobody. A deactivated member is refused."
          },
          "start_date": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO-8601, e.g. 2026-11-01 or 2026-11-01T09:00:00Z. null clears it."
          },
          "due_date": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO-8601, e.g. 2026-11-01 or 2026-11-01T09:00:00Z. null clears it."
          },
          "completed_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO-8601, e.g. 2026-11-01 or 2026-11-01T09:00:00Z. null clears it."
          },
          "estimated_value": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Integer minor units (cents). null clears it."
          },
          "custom_fields": {
            "type": "object",
            "additionalProperties": {},
            "description": "This instance’s own fields, as an object. REPLACES the whole object: read it, change it, send it all back."
          },
          "external_id": {
            "type": [
              "string",
              "null"
            ],
            "minLength": 1,
            "maxLength": 200,
            "description": "YOUR id for this record. On POST, a record you already gave this id is UPDATED instead (200, not 201). On PATCH, sets it; null removes it. Kept per service account: another integration never sees it."
          }
        },
        "required": [
          "title",
          "customer_id"
        ],
        "additionalProperties": false
      },
      "ProjectPatch": {
        "type": "object",
        "properties": {
          "title": {
            "type": "string",
            "minLength": 1,
            "maxLength": 10000
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 10000
          },
          "site_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "One of the customer’s sites, or null."
          },
          "service_type": {
            "type": "string",
            "description": "A SERVICE_TYPE picklist key."
          },
          "status": {
            "type": "string",
            "description": "A PROJECT_STATUS picklist key, checked against the instance’s list exactly as the app checks it. A move is recorded in the project’s History, credited to your service account."
          },
          "status_reason": {
            "type": "string",
            "maxLength": 500,
            "description": "Why the status moved, for the History. Only with `status`."
          },
          "priority": {
            "type": "string",
            "description": "A PROJECT_PRIORITY picklist key."
          },
          "assigned_to": {
            "type": [
              "string",
              "null"
            ],
            "description": "A member id from GET /members, or null for nobody. A deactivated member is refused."
          },
          "start_date": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO-8601, e.g. 2026-11-01 or 2026-11-01T09:00:00Z. null clears it."
          },
          "due_date": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO-8601, e.g. 2026-11-01 or 2026-11-01T09:00:00Z. null clears it."
          },
          "completed_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO-8601, e.g. 2026-11-01 or 2026-11-01T09:00:00Z. null clears it."
          },
          "estimated_value": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Integer minor units (cents). null clears it."
          },
          "custom_fields": {
            "type": "object",
            "additionalProperties": {},
            "description": "This instance’s own fields, as an object. REPLACES the whole object: read it, change it, send it all back."
          },
          "external_id": {
            "type": [
              "string",
              "null"
            ],
            "minLength": 1,
            "maxLength": 200,
            "description": "YOUR id for this record. On POST, a record you already gave this id is UPDATED instead (200, not 201). On PATCH, sets it; null removes it. Kept per service account: another integration never sees it."
          }
        },
        "additionalProperties": false
      },
      "ContactCreate": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 10000
          },
          "email": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 10000
          },
          "phone": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 10000
          },
          "role": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 10000
          },
          "is_primary": {
            "type": "boolean",
            "description": "Making one primary makes the previous primary an ordinary contact."
          },
          "custom_fields": {
            "type": "object",
            "additionalProperties": {},
            "description": "This instance’s own fields, as an object. REPLACES the whole object: read it, change it, send it all back."
          }
        },
        "required": [
          "name"
        ],
        "additionalProperties": false
      },
      "ContactPatch": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 10000
          },
          "email": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 10000
          },
          "phone": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 10000
          },
          "role": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 10000
          },
          "is_primary": {
            "type": "boolean",
            "description": "Making one primary makes the previous primary an ordinary contact."
          },
          "custom_fields": {
            "type": "object",
            "additionalProperties": {},
            "description": "This instance’s own fields, as an object. REPLACES the whole object: read it, change it, send it all back."
          }
        },
        "additionalProperties": false
      },
      "SiteCreate": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 10000
          },
          "address": {
            "type": "object",
            "properties": {
              "line1": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 10000
              },
              "line2": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 10000
              },
              "suburb": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 10000
              },
              "state": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 10000
              },
              "postcode": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 10000
              },
              "country": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 10000
              }
            },
            "additionalProperties": false,
            "description": "Send only the lines you change; a line sent as null is cleared."
          },
          "contact_name": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 10000
          },
          "contact_phone": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 10000
          },
          "access_notes": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 10000
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 10000
          },
          "custom_fields": {
            "type": "object",
            "additionalProperties": {},
            "description": "This instance’s own fields, as an object. REPLACES the whole object: read it, change it, send it all back."
          },
          "external_id": {
            "type": [
              "string",
              "null"
            ],
            "minLength": 1,
            "maxLength": 200,
            "description": "YOUR id for this record. On POST, a record you already gave this id is UPDATED instead (200, not 201). On PATCH, sets it; null removes it. Kept per service account: another integration never sees it."
          }
        },
        "required": [
          "name"
        ],
        "additionalProperties": false
      },
      "SitePatch": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 10000
          },
          "address": {
            "type": "object",
            "properties": {
              "line1": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 10000
              },
              "line2": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 10000
              },
              "suburb": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 10000
              },
              "state": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 10000
              },
              "postcode": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 10000
              },
              "country": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 10000
              }
            },
            "additionalProperties": false,
            "description": "Send only the lines you change; a line sent as null is cleared."
          },
          "contact_name": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 10000
          },
          "contact_phone": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 10000
          },
          "access_notes": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 10000
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 10000
          },
          "custom_fields": {
            "type": "object",
            "additionalProperties": {},
            "description": "This instance’s own fields, as an object. REPLACES the whole object: read it, change it, send it all back."
          },
          "external_id": {
            "type": [
              "string",
              "null"
            ],
            "minLength": 1,
            "maxLength": 200,
            "description": "YOUR id for this record. On POST, a record you already gave this id is UPDATED instead (200, not 201). On PATCH, sets it; null removes it. Kept per service account: another integration never sees it."
          }
        },
        "additionalProperties": false
      },
      "NoteCreate": {
        "type": "object",
        "properties": {
          "body": {
            "type": "string",
            "minLength": 1,
            "maxLength": 10000
          },
          "type": {
            "type": "string",
            "description": "A NOTE_TYPE picklist key. Absent: GENERAL."
          }
        },
        "required": [
          "body"
        ],
        "additionalProperties": false
      },
      "WebhookEventTypes": {
        "type": "object",
        "properties": {
          "event_types": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "type": {
                  "type": "string"
                },
                "object": {
                  "type": "string"
                },
                "when": {
                  "type": "string"
                },
                "summary": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "needs_scope": {
                  "type": "string"
                }
              },
              "required": [
                "type",
                "object",
                "when",
                "summary",
                "needs_scope"
              ]
            }
          }
        },
        "required": [
          "event_types"
        ]
      },
      "WebhookSubscription": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "url": {
            "type": "string"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "event_types": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "state": {
            "type": "string",
            "enum": [
              "active",
              "disabled",
              "disabled_failing"
            ],
            "description": "`disabled_failing`: Alty switched it off after every delivery failed for 3 days."
          },
          "failing_since": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO-8601. Set from the first failure after the last success."
          },
          "last_success_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "disabled_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "disabled_reason": {
            "type": [
              "string",
              "null"
            ]
          },
          "previous_secret_until": {
            "type": [
              "string",
              "null"
            ],
            "description": "After a rotation: until when the previous secret also signs."
          },
          "url_changed_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "The last time the URL was changed after it was made. Admins are told when a key changes it."
          },
          "skip_own_changes": {
            "type": "boolean",
            "description": "No deliveries for this key’s service account’s own API writes."
          },
          "created_at": {
            "type": "string"
          },
          "updated_at": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "url",
          "description",
          "event_types",
          "state",
          "failing_since",
          "last_success_at",
          "disabled_at",
          "disabled_reason",
          "previous_secret_until",
          "url_changed_at",
          "skip_own_changes",
          "created_at",
          "updated_at"
        ]
      },
      "WebhookSubscriptionWithSecret": {
        "allOf": [
          {
            "$ref": "#/components/schemas/WebhookSubscription"
          },
          {
            "type": "object",
            "properties": {
              "secret": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "`whsec_…`. Shown ONCE, here: store it. Every later answer leaves it out. `null` on an `Idempotency-Key` replay (see `secret_omitted`)."
              },
              "secret_omitted": {
                "type": "boolean",
                "enum": [
                  true
                ],
                "description": "Present only on an `idempotent-replayed: true` answer: Alty does not keep the secret to replay it. If you lost it, rotate it."
              }
            },
            "required": [
              "secret"
            ]
          }
        ]
      },
      "WebhookCreate": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "maxLength": 2048,
            "description": "Your receiver. https on port 443, a public host name (no IP addresses, no internal names, nothing on the Alty domain), that does not redirect."
          },
          "event_types": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "customer.created",
                "customer.updated",
                "customer.deleted",
                "contact.created",
                "contact.updated",
                "contact.deleted",
                "site.created",
                "site.updated",
                "site.deleted",
                "lead.created",
                "lead.updated",
                "lead.converted",
                "lead.deleted",
                "project.created",
                "project.updated",
                "project.status_changed",
                "project.deleted",
                "quote.sent",
                "quote.accepted",
                "quote.declined",
                "invoice.issued",
                "invoice.paid",
                "invoice.voided",
                "note.created"
              ],
              "description": "An event type from the catalogue (GET /webhooks/event-types)."
            },
            "minItems": 1,
            "maxItems": 24
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 200,
            "description": "For people: what this receiver is."
          },
          "skip_own_changes": {
            "type": "boolean",
            "description": "true: no deliveries for changes this key’s service account made through the API (its own writes). Default false. Alternatively, leave it off and ignore events whose `actor.service_account_id` is your own."
          }
        },
        "required": [
          "url",
          "event_types"
        ],
        "additionalProperties": false
      },
      "WebhookPatch": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "maxLength": 2048,
            "description": "Your receiver. https on port 443, a public host name (no IP addresses, no internal names, nothing on the Alty domain), that does not redirect."
          },
          "event_types": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "customer.created",
                "customer.updated",
                "customer.deleted",
                "contact.created",
                "contact.updated",
                "contact.deleted",
                "site.created",
                "site.updated",
                "site.deleted",
                "lead.created",
                "lead.updated",
                "lead.converted",
                "lead.deleted",
                "project.created",
                "project.updated",
                "project.status_changed",
                "project.deleted",
                "quote.sent",
                "quote.accepted",
                "quote.declined",
                "invoice.issued",
                "invoice.paid",
                "invoice.voided",
                "note.created"
              ],
              "description": "An event type from the catalogue (GET /webhooks/event-types)."
            },
            "minItems": 1,
            "maxItems": 24
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 200
          },
          "enabled": {
            "type": "boolean",
            "description": "false switches it off; true switches it back on, from NEW events only — nothing that happened while it was off is sent."
          },
          "skip_own_changes": {
            "type": "boolean",
            "description": "true: no deliveries for changes this key’s service account made through the API (its own writes). Default false. Alternatively, leave it off and ignore events whose `actor.service_account_id` is your own."
          }
        },
        "additionalProperties": false
      },
      "WebhookTestResult": {
        "type": "object",
        "properties": {
          "delivery": {
            "$ref": "#/components/schemas/WebhookDelivery"
          },
          "result": {
            "type": "object",
            "properties": {
              "ok": {
                "type": "boolean"
              },
              "status": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "error": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "duration_ms": {
                "type": "integer"
              },
              "response": {
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "required": [
              "ok",
              "status",
              "error",
              "duration_ms",
              "response"
            ]
          }
        },
        "required": [
          "delivery",
          "result"
        ]
      },
      "WebhookDelivery": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "event_id": {
            "type": "string",
            "description": "= the `webhook-id` header. The same on every retry and on a resend."
          },
          "event_type": {
            "type": "string"
          },
          "record": {
            "type": "object",
            "properties": {
              "object": {
                "type": "string"
              },
              "id": {
                "type": "string"
              }
            },
            "required": [
              "object",
              "id"
            ]
          },
          "state": {
            "type": "string",
            "enum": [
              "pending",
              "sending",
              "succeeded",
              "abandoned"
            ]
          },
          "test": {
            "type": "boolean"
          },
          "attempts": {
            "type": "integer"
          },
          "next_attempt_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "last_attempt_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "last_status": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Your receiver’s HTTP status, or null when nothing came back."
          },
          "last_error": {
            "type": [
              "string",
              "null"
            ]
          },
          "last_duration_ms": {
            "type": [
              "integer",
              "null"
            ]
          },
          "last_response": {
            "type": [
              "string",
              "null"
            ],
            "description": "The first 1 KB of your answer."
          },
          "created_at": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "event_id",
          "event_type",
          "record",
          "state",
          "test",
          "attempts",
          "next_attempt_at",
          "last_attempt_at",
          "last_status",
          "last_error",
          "last_duration_ms",
          "last_response",
          "created_at"
        ]
      },
      "ActionList": {
        "type": "object",
        "properties": {
          "actions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Action"
            }
          }
        },
        "required": [
          "actions"
        ]
      },
      "Action": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "record_type": {
            "type": "string"
          },
          "enabled": {
            "type": "boolean",
            "description": "Agents cannot switch an action on; an admin does, in the app."
          },
          "run_role": {
            "type": "string"
          },
          "has_credential": {
            "type": "boolean",
            "description": "Whether an admin has stored its credential. The credential is never returned."
          },
          "automatic": {
            "type": "boolean",
            "description": "It runs on an event (`trigger`) rather than as a button, so it cannot be run by hand."
          },
          "definition": {
            "type": "object",
            "additionalProperties": {},
            "description": "The whole design — the same fields ActionWrite takes — plus `origin` when Alt or an agent drafted it and `switched_off` when an agent switched it off."
          },
          "created_at": {
            "type": "string"
          },
          "updated_at": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "name",
          "description",
          "record_type",
          "enabled",
          "run_role",
          "has_credential",
          "automatic",
          "definition",
          "created_at",
          "updated_at"
        ]
      },
      "ActionRunList": {
        "type": "object",
        "properties": {
          "runs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ActionRun"
            }
          }
        },
        "required": [
          "runs"
        ]
      },
      "ActionRun": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "action_id": {
            "type": "string"
          },
          "action_name": {
            "type": "string"
          },
          "record_type": {
            "type": "string"
          },
          "record_id": {
            "type": "string"
          },
          "run_by": {
            "type": "string",
            "description": "The member or service account that ran it."
          },
          "is_test": {
            "type": "boolean"
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "pending",
              "waiting",
              "ok",
              "failed",
              "partial"
            ]
          },
          "inputs": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "request": {
            "type": "object",
            "properties": {
              "method": {
                "type": "string"
              },
              "url": {
                "type": "string"
              },
              "headers": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "name": {
                      "type": "string"
                    },
                    "value": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "name",
                    "value"
                  ]
                }
              },
              "body": {
                "type": "string"
              }
            },
            "required": [
              "method",
              "url",
              "headers",
              "body"
            ],
            "description": "Exactly what was sent; the credential header reads `[redacted]`."
          },
          "response_status": {
            "type": [
              "number",
              "null"
            ]
          },
          "response_body": {
            "type": [
              "string",
              "null"
            ]
          },
          "outcome": {
            "type": [
              "string",
              "null"
            ]
          },
          "steps": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "step": {
                  "type": "string"
                },
                "ok": {
                  "type": "boolean"
                },
                "detail": {
                  "type": "string"
                }
              },
              "required": [
                "step",
                "ok",
                "detail"
              ]
            }
          },
          "trigger": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "string"
          },
          "completed_at": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "id",
          "action_id",
          "action_name",
          "record_type",
          "record_id",
          "run_by",
          "is_test",
          "status",
          "inputs",
          "request",
          "response_status",
          "response_body",
          "outcome",
          "steps",
          "trigger",
          "created_at",
          "completed_at"
        ]
      },
      "ActionPreview": {
        "type": "object",
        "properties": {
          "request": {
            "type": "object",
            "properties": {
              "method": {
                "type": "string"
              },
              "url": {
                "type": "string"
              },
              "headers": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "name": {
                      "type": "string"
                    },
                    "value": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "name",
                    "value"
                  ]
                }
              },
              "body": {
                "type": "string"
              }
            },
            "required": [
              "method",
              "url",
              "headers",
              "body"
            ]
          }
        },
        "required": [
          "request"
        ]
      },
      "ActionRunRequest": {
        "type": "object",
        "properties": {
          "record_id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64
          },
          "inputs": {
            "type": "object",
            "additionalProperties": {
              "type": "string",
              "maxLength": 10000
            },
            "description": "Values for the action’s inputs. Absent ones take their defaults from the record."
          }
        },
        "required": [
          "record_id"
        ],
        "additionalProperties": false
      },
      "ActionSaved": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Action"
          },
          {
            "type": "object",
            "properties": {
              "credential_dropped": {
                "type": "boolean",
                "description": "true when this change altered what the credential is used for (method, URL, headers, body, hosts, auth, poll, inputs, who may run it, trigger, record type, then_run) and the stored credential was removed: an admin re-enters it after checking the new design."
              }
            },
            "required": [
              "credential_dropped"
            ]
          }
        ]
      },
      "ActionWrite": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 80
          },
          "description": {
            "type": "string",
            "maxLength": 500
          },
          "record_type": {
            "type": "string",
            "enum": [
              "enquiry",
              "customer",
              "project"
            ],
            "description": "What the action is added to: `enquiry` (a lead), `customer` or `project`."
          },
          "run_role": {
            "type": "string",
            "enum": [
              "admin",
              "staff"
            ],
            "description": "Who may run it once it is on. An API key runs as `staff`, so a key can never run an `admin` action."
          },
          "inputs": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string",
                  "maxLength": 40,
                  "description": "Referenced in templates as `input.<name>`."
                },
                "label": {
                  "type": "string",
                  "maxLength": 80
                },
                "default": {
                  "type": "string",
                  "maxLength": 2000,
                  "description": "A text template resolved against the record, e.g. `{{record.contact_email}}`."
                },
                "required": {
                  "type": "boolean"
                },
                "help": {
                  "type": "string",
                  "maxLength": 300
                },
                "choices": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "value": {
                        "type": "string",
                        "maxLength": 40
                      },
                      "label": {
                        "type": "string",
                        "maxLength": 80
                      }
                    },
                    "required": [
                      "value",
                      "label"
                    ],
                    "additionalProperties": false
                  },
                  "maxItems": 40,
                  "description": "Present = a tick-box list; the value is the ticked values, comma-separated."
                }
              },
              "required": [
                "name",
                "label"
              ],
              "additionalProperties": false
            },
            "maxItems": 20
          },
          "request": {
            "type": "object",
            "properties": {
              "method": {
                "type": "string",
                "enum": [
                  "POST",
                  "PUT",
                  "PATCH",
                  "GET",
                  "DELETE"
                ]
              },
              "url": {
                "type": "string",
                "maxLength": 2048,
                "description": "A template; the host must be written out."
              },
              "headers": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "name": {
                      "type": "string",
                      "maxLength": 100
                    },
                    "value": {
                      "type": "string",
                      "maxLength": 2000
                    }
                  },
                  "required": [
                    "name",
                    "value"
                  ],
                  "additionalProperties": false
                },
                "maxItems": 20
              },
              "body": {
                "type": "string",
                "maxLength": 20000
              }
            },
            "required": [
              "method",
              "url",
              "headers",
              "body"
            ],
            "additionalProperties": false
          },
          "auth": {
            "type": "object",
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "none",
                  "bearer",
                  "header",
                  "cf_access"
                ]
              },
              "header_name": {
                "type": "string",
                "maxLength": 100
              }
            },
            "required": [
              "kind"
            ],
            "additionalProperties": false,
            "description": "How the stored credential is sent. The credential itself is set by an admin in the app, never through the API."
          },
          "allowed_hosts": {
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 253
            },
            "maxItems": 20,
            "description": "Hosts it may reach; `*.example.com` matches subdomains. Empty = the URL’s own host."
          },
          "success": {
            "type": "object",
            "properties": {
              "json_path": {
                "type": "string",
                "maxLength": 200
              },
              "equals": {
                "type": "string",
                "maxLength": 200
              }
            },
            "additionalProperties": false
          },
          "steps": {
            "type": "object",
            "properties": {
              "map_fields": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "from": {
                      "type": "string",
                      "maxLength": 200
                    },
                    "to": {
                      "type": "string",
                      "maxLength": 64
                    }
                  },
                  "required": [
                    "from",
                    "to"
                  ],
                  "additionalProperties": false
                },
                "maxItems": 30
              },
              "note": {
                "type": "string",
                "maxLength": 5000
              },
              "set_status": {
                "type": "string",
                "maxLength": 64
              },
              "convert": {
                "type": "boolean"
              },
              "link_customer": {
                "type": "string",
                "maxLength": 200
              },
              "then_run": {
                "type": "string",
                "maxLength": 64,
                "description": "Another action’s id to queue when this one’s steps have run."
              }
            },
            "required": [
              "map_fields"
            ],
            "additionalProperties": false
          },
          "poll": {
            "type": "object",
            "properties": {
              "url": {
                "type": "string",
                "maxLength": 2048
              },
              "json_path": {
                "type": "string",
                "maxLength": 200
              },
              "done_equals": {
                "type": "string",
                "maxLength": 200
              },
              "failed_path": {
                "type": "string",
                "maxLength": 200
              },
              "failed_equals": {
                "type": "string",
                "maxLength": 200
              },
              "interval_s": {
                "type": "number"
              },
              "timeout_s": {
                "type": "number"
              }
            },
            "required": [
              "url",
              "json_path",
              "done_equals",
              "interval_s",
              "timeout_s"
            ],
            "additionalProperties": false,
            "description": "Wait until done: poll this URL (with the same credential) until `json_path` equals `done_equals`."
          },
          "trigger": {
            "anyOf": [
              {
                "type": "object",
                "properties": {
                  "event": {
                    "type": "string",
                    "enum": [
                      "created"
                    ]
                  }
                },
                "required": [
                  "event"
                ],
                "additionalProperties": false
              },
              {
                "type": "object",
                "properties": {
                  "event": {
                    "type": "string",
                    "enum": [
                      "status_changed"
                    ]
                  },
                  "to": {
                    "type": "string",
                    "maxLength": 64
                  }
                },
                "required": [
                  "event",
                  "to"
                ],
                "additionalProperties": false
              }
            ],
            "description": "Run automatically on this event instead of being a button. Saved UNARMED: only an admin switching it on arms it."
          },
          "why": {
            "type": "string",
            "maxLength": 500,
            "description": "For the admin who reviews it: why this action, in a sentence. Shown on the draft."
          }
        },
        "required": [
          "name",
          "record_type",
          "run_role",
          "inputs",
          "request",
          "auth",
          "allowed_hosts",
          "success",
          "steps"
        ],
        "additionalProperties": false
      },
      "Attachment": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "filename": {
            "type": "string"
          },
          "content_type": {
            "type": "string"
          },
          "size_bytes": {
            "type": "number"
          },
          "created_at": {
            "type": "number",
            "description": "Epoch milliseconds."
          }
        },
        "required": [
          "id",
          "filename",
          "content_type",
          "size_bytes",
          "created_at"
        ],
        "additionalProperties": {}
      },
      "AttachmentIn": {
        "type": "object",
        "properties": {
          "filename": {
            "type": "string"
          },
          "content_type": {
            "type": "string"
          },
          "data_base64": {
            "type": "string",
            "description": "The file bytes, base64-encoded."
          }
        },
        "required": [
          "filename",
          "content_type",
          "data_base64"
        ]
      },
      "Ticket": {
        "type": "object",
        "properties": {
          "reference": {
            "type": "string",
            "description": "The human reference, e.g. ACME-CSE-102: a display string in the instance's own format, never to be parsed. Every plane addresses tickets by it."
          },
          "subject": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "description": "A TICKET_STATUS picklist key."
          },
          "ticket_type": {
            "type": "string"
          },
          "requester_email": {
            "type": "string"
          },
          "requester_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "source_ref": {
            "type": "string"
          },
          "priority": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "number",
            "description": "Epoch milliseconds."
          },
          "last_activity_at": {
            "type": "number",
            "description": "Epoch milliseconds."
          },
          "messages": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TicketMessage"
            }
          }
        },
        "required": [
          "reference",
          "subject",
          "status",
          "ticket_type",
          "requester_email",
          "requester_name",
          "source_ref",
          "priority",
          "created_at",
          "last_activity_at"
        ],
        "additionalProperties": {}
      },
      "TicketMessage": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "direction": {
            "type": "string",
            "description": "INBOUND, OUTBOUND or NOTE (notes only on the agent plane)."
          },
          "body": {
            "type": "string"
          },
          "created_at": {
            "type": "number",
            "description": "Epoch milliseconds."
          },
          "attachments": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Attachment"
            }
          }
        },
        "required": [
          "id",
          "direction",
          "body",
          "created_at"
        ],
        "additionalProperties": {}
      },
      "Proposal": {
        "type": "object",
        "properties": {
          "quote_number": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "version": {
            "type": "number"
          },
          "superseded": {
            "type": "boolean"
          },
          "expired": {
            "type": "boolean"
          },
          "already_responded": {
            "type": "boolean"
          },
          "lead_number": {
            "type": [
              "string",
              "null"
            ]
          },
          "contact_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "business_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "number"
          },
          "sent_at": {
            "type": [
              "number",
              "null"
            ]
          },
          "expires_at": {
            "type": [
              "number",
              "null"
            ]
          },
          "decided_at": {
            "type": [
              "number",
              "null"
            ]
          },
          "scope_of_work": {
            "type": [
              "string",
              "null"
            ]
          },
          "terms": {
            "type": [
              "string",
              "null"
            ]
          },
          "currency": {
            "type": "string"
          },
          "line_items": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "description": {
                  "type": "string"
                },
                "quantity": {
                  "type": "number"
                },
                "unit_price": {
                  "type": "number",
                  "description": "Cents."
                },
                "amount": {
                  "type": "number",
                  "description": "Cents: quantity × unit_price, less any line discount."
                }
              },
              "required": [
                "description",
                "quantity",
                "unit_price",
                "amount"
              ]
            }
          },
          "subtotal": {
            "type": "number",
            "description": "Cents."
          },
          "tax_amount": {
            "type": "number",
            "description": "Cents."
          },
          "total": {
            "type": "number",
            "description": "Cents."
          }
        },
        "required": [
          "quote_number",
          "status",
          "version",
          "superseded",
          "expired",
          "already_responded",
          "lead_number",
          "contact_name",
          "business_name",
          "created_at",
          "sent_at",
          "expires_at",
          "decided_at",
          "scope_of_work",
          "terms",
          "currency",
          "line_items",
          "subtotal",
          "tax_amount",
          "total"
        ],
        "additionalProperties": {}
      }
    },
    "parameters": {}
  },
  "paths": {
    "/api/public/v1/whoami": {
      "get": {
        "tags": [
          "Getting started"
        ],
        "summary": "Check a key: which instance, which scopes",
        "description": "Make this your first call. Any live key may call it, whatever its scopes. It returns the instance the key belongs to, the key’s label, scopes and expiry, the service account its changes are credited to, and which of its modules are switched on. A 401 means the key is wrong, revoked or expired, or API access is off for the instance.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": null,
        "x-alty-module": "api",
        "responses": {
          "200": {
            "description": "The key and its instance.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Whoami"
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/guide.md": {
      "get": {
        "tags": [
          "Getting started"
        ],
        "summary": "The integration guide, as markdown",
        "description": "No key needed. The developer guide: auth, scopes, errors, limits, retries, recipes and what the API cannot do. The same document on every instance — it names no data of yours — and the same as https://www.alty.one/developers/guide.md.",
        "security": [],
        "x-alty-scope": null,
        "x-alty-module": "core",
        "responses": {
          "200": {
            "description": "The guide (`text/markdown; charset=utf-8`).",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/api/public/v1/openapi.json": {
      "get": {
        "tags": [
          "Getting started"
        ],
        "summary": "This OpenAPI document",
        "description": "No key needed. The OpenAPI 3.1 description of every endpoint here. The same document on every instance, and the same as https://www.alty.one/developers/openapi.json.",
        "security": [],
        "x-alty-scope": null,
        "x-alty-module": "core",
        "responses": {
          "200": {
            "description": "The OpenAPI document.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/public/v1/customers": {
      "get": {
        "tags": [
          "Records"
        ],
        "summary": "List customers",
        "description": "Records come in order of last change, then id. Follow `next_cursor` until it is null to have them all. Query: `limit` (1–200, default 50), `cursor`, `updated_since` (ISO-8601: only records changed at or after it; to sync, pass the time your last sync STARTED, less a minute — a record you already have may come again, at its latest state), and `include_deleted=true` to also get a tombstone `{id, deleted_at, updated_at, version}` for each record in the Trash, so your copy can be deleted too. A record in the Trash, or under a customer in the Trash, is otherwise invisible. Demo (sample) data is never returned. A record permanently deleted from the Trash leaves no tombstone. A key bound to a service account that has been suspended or deleted gets 403 `service_account_required`. Filter: `external_id` — look a customer up by YOUR id for it (needs a key with a service account).",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "customers:read",
        "x-alty-module": "core",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "description": "1–200, default 50."
            },
            "required": false,
            "description": "1–200, default 50.",
            "name": "limit",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "The `next_cursor` of the previous page."
            },
            "required": false,
            "description": "The `next_cursor` of the previous page.",
            "name": "cursor",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "ISO-8601. Only records changed at or after it. To sync, pass the time your last sync STARTED, less a minute."
            },
            "required": false,
            "description": "ISO-8601. Only records changed at or after it. To sync, pass the time your last sync STARTED, less a minute.",
            "name": "updated_since",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ],
              "description": "`true` adds a tombstone `{id, deleted_at, updated_at, version}` for each record in the Trash. A tombstone carries no `external_id`: keep Alty's id beside yours."
            },
            "required": false,
            "description": "`true` adds a tombstone `{id, deleted_at, updated_at, version}` for each record in the Trash. A tombstone carries no `external_id`: keep Alty's id beside yours.",
            "name": "include_deleted",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "YOUR id for the record (needs a key with a service account). With `include_deleted=true` it also finds the record's tombstone."
            },
            "required": false,
            "description": "YOUR id for the record (needs a key with a service account). With `include_deleted=true` it also finds the record's tombstone.",
            "name": "external_id",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of customers.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerPage"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_limit`, `invalid_cursor`, `invalid_updated_since`, `invalid_include_deleted`. A query parameter the endpoint does not list is ignored.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyForbidden"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      },
      "post": {
        "tags": [
          "Records"
        ],
        "summary": "Create a customer",
        "description": "Runs exactly what the same change in the app runs — the same checks, history, automatic actions, Xero mirror and live update on open screens — credited to your key’s service account. A field you leave out is left alone; a field sent as null is cleared; an unknown field is 400 `unknown_field`. With `external_id`, a customer you already gave that id is updated instead (200).",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "customers:write",
        "x-alty-module": "core",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value."
            },
            "required": false,
            "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value.",
            "name": "idempotency-key",
            "in": "header"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CustomerCreate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Your external_id already named a customer: it was updated (or was already the same).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Customer"
                }
              }
            }
          },
          "201": {
            "description": "Created: the customer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Customer"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_idempotency_key` — the `Idempotency-Key` header is not 1–255 visible ASCII characters. Nothing ran. Also: `invalid_json`, `invalid_field` / `unknown_field` (with `field` and `message`), `invalid_if_match`, or the app’s own refusal in words (an unknown status, a missing name).",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/InvalidIdempotencyKey"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it). Also: `service_account_required`: the key is not bound to an active service account (none, or suspended or deleted).",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "missing_scope"
                          ]
                        },
                        "needs": {
                          "type": "string",
                          "description": "The scope this endpoint needs. Ask your Alty admin for a key that has it."
                        }
                      },
                      "required": [
                        "error",
                        "needs"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "module_off"
                          ]
                        },
                        "module": {
                          "type": "string",
                          "description": "The module that is switched off for this instance, e.g. `support`."
                        }
                      },
                      "required": [
                        "error",
                        "module"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Not found — the same answer whether it does not exist or is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_conflict` — this `Idempotency-Key` was already used with a different request (method, path or body): use a new value for a new request. `idempotency_in_progress` — the first request with this value is still running: wait for `Retry-After` (seconds) and retry with the SAME value. A completed request is never a 409: a retry with the same value and body gets the original status code and body back, with the header `idempotent-replayed: true`. Also: `external_id_taken` (another of your records holds that external id) or `in_trash` (the record holding it is in the Trash: an admin restores it in the app, or deletes it forever to free the id. The API cannot change a record in the Trash).",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/IdempotencyConflict"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/customers/{id}": {
      "get": {
        "tags": [
          "Records"
        ],
        "summary": "Get one customer",
        "description": "One customer by id. 404 when it does not exist, is in the Trash, or belongs to a customer in the Trash.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "customers:read",
        "x-alty-module": "core",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The customer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Customer"
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyForbidden"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the same answer whether it does not exist or is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      },
      "patch": {
        "tags": [
          "Records"
        ],
        "summary": "Change a customer",
        "description": "Runs exactly what the same change in the app runs — the same checks, history, automatic actions, Xero mirror and live update on open screens — credited to your key’s service account. A field you leave out is left alone; a field sent as null is cleared; an unknown field is 400 `unknown_field`. Send `If-Match: <version>` (the `version` you last read) to refuse overwriting a newer edit: 412 `version_conflict` with `current`, and nothing is written. A PATCH that changes nothing changes nothing: no new version, no history, no Xero push.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "customers:write",
        "x-alty-module": "core",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "id",
            "in": "path"
          },
          {
            "schema": {
              "type": "string",
              "description": "Optional: the `version` you last read. If the record has changed since, nothing is written and you get 412 `version_conflict` with `current`. Without it, the last write wins, as in the app."
            },
            "required": false,
            "description": "Optional: the `version` you last read. If the record has changed since, nothing is written and you get 412 `version_conflict` with `current`. Without it, the last write wins, as in the app.",
            "name": "if-match",
            "in": "header"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CustomerPatch"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The customer, as it is now.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Customer"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_json`, `invalid_field` / `unknown_field` (with `field` and `message`), `invalid_if_match`, or the app’s own refusal in words (an unknown status, a missing name).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it). Also: `service_account_required`: the key is not bound to an active service account (none, or suspended or deleted).",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "missing_scope"
                          ]
                        },
                        "needs": {
                          "type": "string",
                          "description": "The scope this endpoint needs. Ask your Alty admin for a key that has it."
                        }
                      },
                      "required": [
                        "error",
                        "needs"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "module_off"
                          ]
                        },
                        "module": {
                          "type": "string",
                          "description": "The module that is switched off for this instance, e.g. `support`."
                        }
                      },
                      "required": [
                        "error",
                        "module"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Not found — the same answer whether it does not exist or is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`external_id_taken` (another of your records holds that external id) or `in_trash` (the record holding it is in the Trash: an admin restores it in the app, or deletes it forever to free the id. The API cannot change a record in the Trash).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "412": {
            "description": "The record changed since the version in If-Match. Nothing was written.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VersionConflict"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      },
      "delete": {
        "tags": [
          "Records"
        ],
        "summary": "Delete a customer",
        "description": "Moves it to the Trash, as the app’s Delete does; its Xero contact is archived, never deleted. Restore is in the app. With `include_deleted=true`, lists then show its tombstone. Takes `If-Match` like PATCH.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "customers:write",
        "x-alty-module": "core",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "id",
            "in": "path"
          },
          {
            "schema": {
              "type": "string",
              "description": "Optional: the `version` you last read. If the record has changed since, nothing is written and you get 412 `version_conflict` with `current`. Without it, the last write wins, as in the app."
            },
            "required": false,
            "description": "Optional: the `version` you last read. If the record has changed since, nothing is written and you get 412 `version_conflict` with `current`. Without it, the last write wins, as in the app.",
            "name": "if-match",
            "in": "header"
          }
        ],
        "responses": {
          "200": {
            "description": "In the Trash.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Deleted"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_json`, `invalid_field` / `unknown_field` (with `field` and `message`), `invalid_if_match`, or the app’s own refusal in words (an unknown status, a missing name).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it). Also: `service_account_required`: the key is not bound to an active service account (none, or suspended or deleted).",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "missing_scope"
                          ]
                        },
                        "needs": {
                          "type": "string",
                          "description": "The scope this endpoint needs. Ask your Alty admin for a key that has it."
                        }
                      },
                      "required": [
                        "error",
                        "needs"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "module_off"
                          ]
                        },
                        "module": {
                          "type": "string",
                          "description": "The module that is switched off for this instance, e.g. `support`."
                        }
                      },
                      "required": [
                        "error",
                        "module"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Not found — the same answer whether it does not exist or is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "412": {
            "description": "The record changed since the version in If-Match. Nothing was written.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VersionConflict"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/leads": {
      "get": {
        "tags": [
          "Records"
        ],
        "summary": "List leads",
        "description": "Records come in order of last change, then id. Follow `next_cursor` until it is null to have them all. Query: `limit` (1–200, default 50), `cursor`, `updated_since` (ISO-8601: only records changed at or after it; to sync, pass the time your last sync STARTED, less a minute — a record you already have may come again, at its latest state), and `include_deleted=true` to also get a tombstone `{id, deleted_at, updated_at, version}` for each record in the Trash, so your copy can be deleted too. A record in the Trash, or under a customer in the Trash, is otherwise invisible. Demo (sample) data is never returned. A record permanently deleted from the Trash leaves no tombstone. A key bound to a service account that has been suspended or deleted gets 403 `service_account_required`. Filters: `customer_id`, `status` (a picklist key), `external_id`. Needs the Leads module in use.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "leads:read",
        "x-alty-module": "enquiries",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "description": "1–200, default 50."
            },
            "required": false,
            "description": "1–200, default 50.",
            "name": "limit",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "The `next_cursor` of the previous page."
            },
            "required": false,
            "description": "The `next_cursor` of the previous page.",
            "name": "cursor",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "ISO-8601. Only records changed at or after it. To sync, pass the time your last sync STARTED, less a minute."
            },
            "required": false,
            "description": "ISO-8601. Only records changed at or after it. To sync, pass the time your last sync STARTED, less a minute.",
            "name": "updated_since",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ],
              "description": "`true` adds a tombstone `{id, deleted_at, updated_at, version}` for each record in the Trash. A tombstone carries no `external_id`: keep Alty's id beside yours."
            },
            "required": false,
            "description": "`true` adds a tombstone `{id, deleted_at, updated_at, version}` for each record in the Trash. A tombstone carries no `external_id`: keep Alty's id beside yours.",
            "name": "include_deleted",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "Only this customer's records."
            },
            "required": false,
            "description": "Only this customer's records.",
            "name": "customer_id",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "A status picklist key."
            },
            "required": false,
            "description": "A status picklist key.",
            "name": "status",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "YOUR id for the record (needs a key with a service account). With `include_deleted=true` it also finds the record's tombstone."
            },
            "required": false,
            "description": "YOUR id for the record (needs a key with a service account). With `include_deleted=true` it also finds the record's tombstone.",
            "name": "external_id",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of leads.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LeadPage"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_limit`, `invalid_cursor`, `invalid_updated_since`, `invalid_include_deleted`. A query parameter the endpoint does not list is ignored.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyForbidden"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      },
      "post": {
        "tags": [
          "Records"
        ],
        "summary": "Create a lead",
        "description": "Runs exactly what the same change in the app runs — the same checks, history, automatic actions, Xero mirror and live update on open screens — credited to your key’s service account. A field you leave out is left alone; a field sent as null is cleared; an unknown field is 400 `unknown_field`. A plain create: no spam check and no auto-reply (that is the website intake’s, `enquiry:intake`). With `external_id`, a lead you already gave that id is updated instead (200). Needs the Leads module in use.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "leads:write",
        "x-alty-module": "enquiries",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value."
            },
            "required": false,
            "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value.",
            "name": "idempotency-key",
            "in": "header"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LeadCreate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Your external_id already named a lead: it was updated (or was already the same).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Lead"
                }
              }
            }
          },
          "201": {
            "description": "Created: the lead.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Lead"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_idempotency_key` — the `Idempotency-Key` header is not 1–255 visible ASCII characters. Nothing ran. Also: `invalid_json`, `invalid_field` / `unknown_field` (with `field` and `message`), `invalid_if_match`, or the app’s own refusal in words (an unknown status, a missing name).",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/InvalidIdempotencyKey"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it). Also: `service_account_required`: the key is not bound to an active service account (none, or suspended or deleted).",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "missing_scope"
                          ]
                        },
                        "needs": {
                          "type": "string",
                          "description": "The scope this endpoint needs. Ask your Alty admin for a key that has it."
                        }
                      },
                      "required": [
                        "error",
                        "needs"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "module_off"
                          ]
                        },
                        "module": {
                          "type": "string",
                          "description": "The module that is switched off for this instance, e.g. `support`."
                        }
                      },
                      "required": [
                        "error",
                        "module"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Not found — the same answer whether it does not exist or is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_conflict` — this `Idempotency-Key` was already used with a different request (method, path or body): use a new value for a new request. `idempotency_in_progress` — the first request with this value is still running: wait for `Retry-After` (seconds) and retry with the SAME value. A completed request is never a 409: a retry with the same value and body gets the original status code and body back, with the header `idempotent-replayed: true`. Also: `external_id_taken` (another of your records holds that external id) or `in_trash` (the record holding it is in the Trash: an admin restores it in the app, or deletes it forever to free the id. The API cannot change a record in the Trash).",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/IdempotencyConflict"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/leads/{id}": {
      "get": {
        "tags": [
          "Records"
        ],
        "summary": "Get one lead",
        "description": "One lead by id. 404 when it does not exist, is in the Trash, or belongs to a customer in the Trash.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "leads:read",
        "x-alty-module": "enquiries",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The lead.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Lead"
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyForbidden"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the same answer whether it does not exist or is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      },
      "patch": {
        "tags": [
          "Records"
        ],
        "summary": "Change a lead",
        "description": "Runs exactly what the same change in the app runs — the same checks, history, automatic actions, Xero mirror and live update on open screens — credited to your key’s service account. A field you leave out is left alone; a field sent as null is cleared; an unknown field is 400 `unknown_field`. Send `If-Match: <version>` (the `version` you last read) to refuse overwriting a newer edit: 412 `version_conflict` with `current`, and nothing is written. A PATCH that changes nothing changes nothing: no new version, no history, no Xero push.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "leads:write",
        "x-alty-module": "enquiries",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "id",
            "in": "path"
          },
          {
            "schema": {
              "type": "string",
              "description": "Optional: the `version` you last read. If the record has changed since, nothing is written and you get 412 `version_conflict` with `current`. Without it, the last write wins, as in the app."
            },
            "required": false,
            "description": "Optional: the `version` you last read. If the record has changed since, nothing is written and you get 412 `version_conflict` with `current`. Without it, the last write wins, as in the app.",
            "name": "if-match",
            "in": "header"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LeadPatch"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The lead, as it is now.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Lead"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_json`, `invalid_field` / `unknown_field` (with `field` and `message`), `invalid_if_match`, or the app’s own refusal in words (an unknown status, a missing name).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it). Also: `service_account_required`: the key is not bound to an active service account (none, or suspended or deleted).",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "missing_scope"
                          ]
                        },
                        "needs": {
                          "type": "string",
                          "description": "The scope this endpoint needs. Ask your Alty admin for a key that has it."
                        }
                      },
                      "required": [
                        "error",
                        "needs"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "module_off"
                          ]
                        },
                        "module": {
                          "type": "string",
                          "description": "The module that is switched off for this instance, e.g. `support`."
                        }
                      },
                      "required": [
                        "error",
                        "module"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Not found — the same answer whether it does not exist or is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`external_id_taken` (another of your records holds that external id) or `in_trash` (the record holding it is in the Trash: an admin restores it in the app, or deletes it forever to free the id. The API cannot change a record in the Trash).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "412": {
            "description": "The record changed since the version in If-Match. Nothing was written.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VersionConflict"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      },
      "delete": {
        "tags": [
          "Records"
        ],
        "summary": "Delete a lead",
        "description": "Moves it to the Trash. Needs the Leads module in use. With `include_deleted=true`, lists then show its tombstone. Takes `If-Match` like PATCH.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "leads:write",
        "x-alty-module": "enquiries",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "id",
            "in": "path"
          },
          {
            "schema": {
              "type": "string",
              "description": "Optional: the `version` you last read. If the record has changed since, nothing is written and you get 412 `version_conflict` with `current`. Without it, the last write wins, as in the app."
            },
            "required": false,
            "description": "Optional: the `version` you last read. If the record has changed since, nothing is written and you get 412 `version_conflict` with `current`. Without it, the last write wins, as in the app.",
            "name": "if-match",
            "in": "header"
          }
        ],
        "responses": {
          "200": {
            "description": "In the Trash.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Deleted"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_json`, `invalid_field` / `unknown_field` (with `field` and `message`), `invalid_if_match`, or the app’s own refusal in words (an unknown status, a missing name).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it). Also: `service_account_required`: the key is not bound to an active service account (none, or suspended or deleted).",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "missing_scope"
                          ]
                        },
                        "needs": {
                          "type": "string",
                          "description": "The scope this endpoint needs. Ask your Alty admin for a key that has it."
                        }
                      },
                      "required": [
                        "error",
                        "needs"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "module_off"
                          ]
                        },
                        "module": {
                          "type": "string",
                          "description": "The module that is switched off for this instance, e.g. `support`."
                        }
                      },
                      "required": [
                        "error",
                        "module"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Not found — the same answer whether it does not exist or is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "412": {
            "description": "The record changed since the version in If-Match. Nothing was written.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VersionConflict"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/projects": {
      "get": {
        "tags": [
          "Records"
        ],
        "summary": "List projects",
        "description": "Records come in order of last change, then id. Follow `next_cursor` until it is null to have them all. Query: `limit` (1–200, default 50), `cursor`, `updated_since` (ISO-8601: only records changed at or after it; to sync, pass the time your last sync STARTED, less a minute — a record you already have may come again, at its latest state), and `include_deleted=true` to also get a tombstone `{id, deleted_at, updated_at, version}` for each record in the Trash, so your copy can be deleted too. A record in the Trash, or under a customer in the Trash, is otherwise invisible. Demo (sample) data is never returned. A record permanently deleted from the Trash leaves no tombstone. A key bound to a service account that has been suspended or deleted gets 403 `service_account_required`. Filters: `customer_id`, `status` (a picklist key), `external_id`. Needs the Projects module in use. The app may call them Jobs; the API always says project.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "projects:read",
        "x-alty-module": "projects",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "description": "1–200, default 50."
            },
            "required": false,
            "description": "1–200, default 50.",
            "name": "limit",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "The `next_cursor` of the previous page."
            },
            "required": false,
            "description": "The `next_cursor` of the previous page.",
            "name": "cursor",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "ISO-8601. Only records changed at or after it. To sync, pass the time your last sync STARTED, less a minute."
            },
            "required": false,
            "description": "ISO-8601. Only records changed at or after it. To sync, pass the time your last sync STARTED, less a minute.",
            "name": "updated_since",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ],
              "description": "`true` adds a tombstone `{id, deleted_at, updated_at, version}` for each record in the Trash. A tombstone carries no `external_id`: keep Alty's id beside yours."
            },
            "required": false,
            "description": "`true` adds a tombstone `{id, deleted_at, updated_at, version}` for each record in the Trash. A tombstone carries no `external_id`: keep Alty's id beside yours.",
            "name": "include_deleted",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "Only this customer's records."
            },
            "required": false,
            "description": "Only this customer's records.",
            "name": "customer_id",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "A status picklist key."
            },
            "required": false,
            "description": "A status picklist key.",
            "name": "status",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "YOUR id for the record (needs a key with a service account). With `include_deleted=true` it also finds the record's tombstone."
            },
            "required": false,
            "description": "YOUR id for the record (needs a key with a service account). With `include_deleted=true` it also finds the record's tombstone.",
            "name": "external_id",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of projects.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectPage"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_limit`, `invalid_cursor`, `invalid_updated_since`, `invalid_include_deleted`. A query parameter the endpoint does not list is ignored.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyForbidden"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      },
      "post": {
        "tags": [
          "Records"
        ],
        "summary": "Create a project",
        "description": "Runs exactly what the same change in the app runs — the same checks, history, automatic actions, Xero mirror and live update on open screens — credited to your key’s service account. A field you leave out is left alone; a field sent as null is cleared; an unknown field is 400 `unknown_field`. With `external_id`, a project you already gave that id is updated instead (200). Needs the Projects module in use.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "projects:write",
        "x-alty-module": "projects",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value."
            },
            "required": false,
            "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value.",
            "name": "idempotency-key",
            "in": "header"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProjectCreate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Your external_id already named a project: it was updated (or was already the same).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Project"
                }
              }
            }
          },
          "201": {
            "description": "Created: the project.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Project"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_idempotency_key` — the `Idempotency-Key` header is not 1–255 visible ASCII characters. Nothing ran. Also: `invalid_json`, `invalid_field` / `unknown_field` (with `field` and `message`), `invalid_if_match`, or the app’s own refusal in words (an unknown status, a missing name).",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/InvalidIdempotencyKey"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it). Also: `service_account_required`: the key is not bound to an active service account (none, or suspended or deleted).",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "missing_scope"
                          ]
                        },
                        "needs": {
                          "type": "string",
                          "description": "The scope this endpoint needs. Ask your Alty admin for a key that has it."
                        }
                      },
                      "required": [
                        "error",
                        "needs"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "module_off"
                          ]
                        },
                        "module": {
                          "type": "string",
                          "description": "The module that is switched off for this instance, e.g. `support`."
                        }
                      },
                      "required": [
                        "error",
                        "module"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Not found — the same answer whether it does not exist or is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_conflict` — this `Idempotency-Key` was already used with a different request (method, path or body): use a new value for a new request. `idempotency_in_progress` — the first request with this value is still running: wait for `Retry-After` (seconds) and retry with the SAME value. A completed request is never a 409: a retry with the same value and body gets the original status code and body back, with the header `idempotent-replayed: true`. Also: `external_id_taken` (another of your records holds that external id) or `in_trash` (the record holding it is in the Trash: an admin restores it in the app, or deletes it forever to free the id. The API cannot change a record in the Trash).",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/IdempotencyConflict"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/projects/{id}": {
      "get": {
        "tags": [
          "Records"
        ],
        "summary": "Get one project",
        "description": "One project by id. 404 when it does not exist, is in the Trash, or belongs to a customer in the Trash.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "projects:read",
        "x-alty-module": "projects",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The project.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Project"
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyForbidden"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the same answer whether it does not exist or is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      },
      "patch": {
        "tags": [
          "Records"
        ],
        "summary": "Change a project",
        "description": "Runs exactly what the same change in the app runs — the same checks, history, automatic actions, Xero mirror and live update on open screens — credited to your key’s service account. A field you leave out is left alone; a field sent as null is cleared; an unknown field is 400 `unknown_field`. Send `If-Match: <version>` (the `version` you last read) to refuse overwriting a newer edit: 412 `version_conflict` with `current`, and nothing is written. A PATCH that changes nothing changes nothing: no new version, no history, no Xero push.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "projects:write",
        "x-alty-module": "projects",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "id",
            "in": "path"
          },
          {
            "schema": {
              "type": "string",
              "description": "Optional: the `version` you last read. If the record has changed since, nothing is written and you get 412 `version_conflict` with `current`. Without it, the last write wins, as in the app."
            },
            "required": false,
            "description": "Optional: the `version` you last read. If the record has changed since, nothing is written and you get 412 `version_conflict` with `current`. Without it, the last write wins, as in the app.",
            "name": "if-match",
            "in": "header"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProjectPatch"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The project, as it is now.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Project"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_json`, `invalid_field` / `unknown_field` (with `field` and `message`), `invalid_if_match`, or the app’s own refusal in words (an unknown status, a missing name).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it). Also: `service_account_required`: the key is not bound to an active service account (none, or suspended or deleted).",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "missing_scope"
                          ]
                        },
                        "needs": {
                          "type": "string",
                          "description": "The scope this endpoint needs. Ask your Alty admin for a key that has it."
                        }
                      },
                      "required": [
                        "error",
                        "needs"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "module_off"
                          ]
                        },
                        "module": {
                          "type": "string",
                          "description": "The module that is switched off for this instance, e.g. `support`."
                        }
                      },
                      "required": [
                        "error",
                        "module"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Not found — the same answer whether it does not exist or is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`external_id_taken` (another of your records holds that external id) or `in_trash` (the record holding it is in the Trash: an admin restores it in the app, or deletes it forever to free the id. The API cannot change a record in the Trash).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "412": {
            "description": "The record changed since the version in If-Match. Nothing was written.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VersionConflict"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      },
      "delete": {
        "tags": [
          "Records"
        ],
        "summary": "Delete a project",
        "description": "Moves it to the Trash. Needs the Projects module in use. With `include_deleted=true`, lists then show its tombstone. Takes `If-Match` like PATCH.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "projects:write",
        "x-alty-module": "projects",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "id",
            "in": "path"
          },
          {
            "schema": {
              "type": "string",
              "description": "Optional: the `version` you last read. If the record has changed since, nothing is written and you get 412 `version_conflict` with `current`. Without it, the last write wins, as in the app."
            },
            "required": false,
            "description": "Optional: the `version` you last read. If the record has changed since, nothing is written and you get 412 `version_conflict` with `current`. Without it, the last write wins, as in the app.",
            "name": "if-match",
            "in": "header"
          }
        ],
        "responses": {
          "200": {
            "description": "In the Trash.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Deleted"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_json`, `invalid_field` / `unknown_field` (with `field` and `message`), `invalid_if_match`, or the app’s own refusal in words (an unknown status, a missing name).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it). Also: `service_account_required`: the key is not bound to an active service account (none, or suspended or deleted).",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "missing_scope"
                          ]
                        },
                        "needs": {
                          "type": "string",
                          "description": "The scope this endpoint needs. Ask your Alty admin for a key that has it."
                        }
                      },
                      "required": [
                        "error",
                        "needs"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "module_off"
                          ]
                        },
                        "module": {
                          "type": "string",
                          "description": "The module that is switched off for this instance, e.g. `support`."
                        }
                      },
                      "required": [
                        "error",
                        "module"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Not found — the same answer whether it does not exist or is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "412": {
            "description": "The record changed since the version in If-Match. Nothing was written.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VersionConflict"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/quotes": {
      "get": {
        "tags": [
          "Records"
        ],
        "summary": "List quotes",
        "description": "Records come in order of last change, then id. Follow `next_cursor` until it is null to have them all. Query: `limit` (1–200, default 50), `cursor`, `updated_since` (ISO-8601: only records changed at or after it; to sync, pass the time your last sync STARTED, less a minute — a record you already have may come again, at its latest state), and `include_deleted=true` to also get a tombstone `{id, deleted_at, updated_at, version}` for each record in the Trash, so your copy can be deleted too. A record in the Trash, or under a customer in the Trash, is otherwise invisible. Demo (sample) data is never returned. A record permanently deleted from the Trash leaves no tombstone. A key bound to a service account that has been suspended or deleted gets 403 `service_account_required`. Filters: `customer_id`, `project_id`, `status`. Read-only: quotes are made in the app. Lines come with GET /quotes/{id}.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "quotes:read",
        "x-alty-module": "quotes",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "description": "1–200, default 50."
            },
            "required": false,
            "description": "1–200, default 50.",
            "name": "limit",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "The `next_cursor` of the previous page."
            },
            "required": false,
            "description": "The `next_cursor` of the previous page.",
            "name": "cursor",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "ISO-8601. Only records changed at or after it. To sync, pass the time your last sync STARTED, less a minute."
            },
            "required": false,
            "description": "ISO-8601. Only records changed at or after it. To sync, pass the time your last sync STARTED, less a minute.",
            "name": "updated_since",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ],
              "description": "`true` adds a tombstone `{id, deleted_at, updated_at, version}` for each record in the Trash. A tombstone carries no `external_id`: keep Alty's id beside yours."
            },
            "required": false,
            "description": "`true` adds a tombstone `{id, deleted_at, updated_at, version}` for each record in the Trash. A tombstone carries no `external_id`: keep Alty's id beside yours.",
            "name": "include_deleted",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "Only this customer's records."
            },
            "required": false,
            "description": "Only this customer's records.",
            "name": "customer_id",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "Only this project's records."
            },
            "required": false,
            "description": "Only this project's records.",
            "name": "project_id",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "A status picklist key."
            },
            "required": false,
            "description": "A status picklist key.",
            "name": "status",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of quotes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuotePage"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_limit`, `invalid_cursor`, `invalid_updated_since`, `invalid_include_deleted`. A query parameter the endpoint does not list is ignored.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyForbidden"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/quotes/{id}": {
      "get": {
        "tags": [
          "Records"
        ],
        "summary": "Get one quote",
        "description": "One quote by id. With its lines. 404 when it does not exist, is in the Trash, or belongs to a customer in the Trash.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "quotes:read",
        "x-alty-module": "quotes",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The quote.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Quote"
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyForbidden"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the same answer whether it does not exist or is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/invoices": {
      "get": {
        "tags": [
          "Records"
        ],
        "summary": "List invoices",
        "description": "Records come in order of last change, then id. Follow `next_cursor` until it is null to have them all. Query: `limit` (1–200, default 50), `cursor`, `updated_since` (ISO-8601: only records changed at or after it; to sync, pass the time your last sync STARTED, less a minute — a record you already have may come again, at its latest state), and `include_deleted=true` to also get a tombstone `{id, deleted_at, updated_at, version}` for each record in the Trash, so your copy can be deleted too. A record in the Trash, or under a customer in the Trash, is otherwise invisible. Demo (sample) data is never returned. A record permanently deleted from the Trash leaves no tombstone. A key bound to a service account that has been suspended or deleted gets 403 `service_account_required`. Filters: `customer_id`, `project_id`, `status`. Read-only: billing stays in the app, where the Xero and Books rules apply. Lines come with GET /invoices/{id}.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "invoices:read",
        "x-alty-module": "invoices",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "description": "1–200, default 50."
            },
            "required": false,
            "description": "1–200, default 50.",
            "name": "limit",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "The `next_cursor` of the previous page."
            },
            "required": false,
            "description": "The `next_cursor` of the previous page.",
            "name": "cursor",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "ISO-8601. Only records changed at or after it. To sync, pass the time your last sync STARTED, less a minute."
            },
            "required": false,
            "description": "ISO-8601. Only records changed at or after it. To sync, pass the time your last sync STARTED, less a minute.",
            "name": "updated_since",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ],
              "description": "`true` adds a tombstone `{id, deleted_at, updated_at, version}` for each record in the Trash. A tombstone carries no `external_id`: keep Alty's id beside yours."
            },
            "required": false,
            "description": "`true` adds a tombstone `{id, deleted_at, updated_at, version}` for each record in the Trash. A tombstone carries no `external_id`: keep Alty's id beside yours.",
            "name": "include_deleted",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "Only this customer's records."
            },
            "required": false,
            "description": "Only this customer's records.",
            "name": "customer_id",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "Only this project's records."
            },
            "required": false,
            "description": "Only this project's records.",
            "name": "project_id",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "A status picklist key."
            },
            "required": false,
            "description": "A status picklist key.",
            "name": "status",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of invoices.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoicePage"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_limit`, `invalid_cursor`, `invalid_updated_since`, `invalid_include_deleted`. A query parameter the endpoint does not list is ignored.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyForbidden"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/invoices/{id}": {
      "get": {
        "tags": [
          "Records"
        ],
        "summary": "Get one invoice",
        "description": "One invoice by id. With its lines and what is still owed. 404 when it does not exist, is in the Trash, or belongs to a customer in the Trash.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "invoices:read",
        "x-alty-module": "invoices",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The invoice.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Invoice"
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyForbidden"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the same answer whether it does not exist or is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/sites": {
      "get": {
        "tags": [
          "Records"
        ],
        "summary": "List sites, across all customers",
        "description": "Every customer's sites, as a change feed. Records come in order of last change, then id. Follow `next_cursor` until it is null to have them all. Query: `limit` (1–200, default 50), `cursor`, `updated_since` (ISO-8601: only records changed at or after it; to sync, pass the time your last sync STARTED, less a minute — a record you already have may come again, at its latest state), and `include_deleted=true` to also get a tombstone `{id, deleted_at, updated_at, version}` for each record in the Trash, so your copy can be deleted too. A record in the Trash, or under a customer in the Trash, is otherwise invisible. Demo (sample) data is never returned. A record permanently deleted from the Trash leaves no tombstone. A key bound to a service account that has been suspended or deleted gets 403 `service_account_required`. Filters: `customer_id`, `external_id`. Needs the Sites module in use.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "customers:read",
        "x-alty-module": "sites",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "description": "1–200, default 50."
            },
            "required": false,
            "description": "1–200, default 50.",
            "name": "limit",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "The `next_cursor` of the previous page."
            },
            "required": false,
            "description": "The `next_cursor` of the previous page.",
            "name": "cursor",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "ISO-8601. Only records changed at or after it. To sync, pass the time your last sync STARTED, less a minute."
            },
            "required": false,
            "description": "ISO-8601. Only records changed at or after it. To sync, pass the time your last sync STARTED, less a minute.",
            "name": "updated_since",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ],
              "description": "`true` adds a tombstone `{id, deleted_at, updated_at, version}` for each record in the Trash. A tombstone carries no `external_id`: keep Alty's id beside yours."
            },
            "required": false,
            "description": "`true` adds a tombstone `{id, deleted_at, updated_at, version}` for each record in the Trash. A tombstone carries no `external_id`: keep Alty's id beside yours.",
            "name": "include_deleted",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "Only this customer's records."
            },
            "required": false,
            "description": "Only this customer's records.",
            "name": "customer_id",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "YOUR id for the record (needs a key with a service account). With `include_deleted=true` it also finds the record's tombstone."
            },
            "required": false,
            "description": "YOUR id for the record (needs a key with a service account). With `include_deleted=true` it also finds the record's tombstone.",
            "name": "external_id",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of sites.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SitePage"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_limit`, `invalid_cursor`, `invalid_updated_since`, `invalid_include_deleted`. A query parameter the endpoint does not list is ignored.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyForbidden"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/customers/{id}/contacts": {
      "get": {
        "tags": [
          "Records"
        ],
        "summary": "List a customer's contacts",
        "description": "All of a customer's contacts, paged like every list. A removed contact is gone, with no tombstone: compare the whole list with your copy. `include_deleted` is accepted and changes nothing. 404 when the customer is not visible.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "customers:read",
        "x-alty-module": "core",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "id",
            "in": "path"
          },
          {
            "schema": {
              "type": "string",
              "description": "1–200, default 50."
            },
            "required": false,
            "description": "1–200, default 50.",
            "name": "limit",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "The `next_cursor` of the previous page."
            },
            "required": false,
            "description": "The `next_cursor` of the previous page.",
            "name": "cursor",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "ISO-8601. Only records changed at or after it. To sync, pass the time your last sync STARTED, less a minute."
            },
            "required": false,
            "description": "ISO-8601. Only records changed at or after it. To sync, pass the time your last sync STARTED, less a minute.",
            "name": "updated_since",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ],
              "description": "`true` adds a tombstone `{id, deleted_at, updated_at, version}` for each record in the Trash. A tombstone carries no `external_id`: keep Alty's id beside yours."
            },
            "required": false,
            "description": "`true` adds a tombstone `{id, deleted_at, updated_at, version}` for each record in the Trash. A tombstone carries no `external_id`: keep Alty's id beside yours.",
            "name": "include_deleted",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of contacts.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactPage"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_limit`, `invalid_cursor`, `invalid_updated_since`, `invalid_include_deleted`. A query parameter the endpoint does not list is ignored.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyForbidden"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the same answer whether it does not exist or is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      },
      "post": {
        "tags": [
          "Records"
        ],
        "summary": "Add a contact to a customer",
        "description": "Runs exactly what the same change in the app runs — the same checks, history, automatic actions, Xero mirror and live update on open screens — credited to your key’s service account. A field you leave out is left alone; a field sent as null is cleared; an unknown field is 400 `unknown_field`. 404 when the customer is not visible.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "customers:write",
        "x-alty-module": "core",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "id",
            "in": "path"
          },
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value."
            },
            "required": false,
            "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value.",
            "name": "idempotency-key",
            "in": "header"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ContactCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created: the contact.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Contact"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_idempotency_key` — the `Idempotency-Key` header is not 1–255 visible ASCII characters. Nothing ran. Also: `invalid_json`, `invalid_field` / `unknown_field` (with `field` and `message`), `invalid_if_match`, or the app’s own refusal in words (an unknown status, a missing name).",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/InvalidIdempotencyKey"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it). Also: `service_account_required`: the key is not bound to an active service account (none, or suspended or deleted).",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "missing_scope"
                          ]
                        },
                        "needs": {
                          "type": "string",
                          "description": "The scope this endpoint needs. Ask your Alty admin for a key that has it."
                        }
                      },
                      "required": [
                        "error",
                        "needs"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "module_off"
                          ]
                        },
                        "module": {
                          "type": "string",
                          "description": "The module that is switched off for this instance, e.g. `support`."
                        }
                      },
                      "required": [
                        "error",
                        "module"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Not found — the same answer whether it does not exist or is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_conflict` — this `Idempotency-Key` was already used with a different request (method, path or body): use a new value for a new request. `idempotency_in_progress` — the first request with this value is still running: wait for `Retry-After` (seconds) and retry with the SAME value. A completed request is never a 409: a retry with the same value and body gets the original status code and body back, with the header `idempotent-replayed: true`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotencyConflict"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/customers/{id}/sites": {
      "get": {
        "tags": [
          "Records"
        ],
        "summary": "List a customer's sites",
        "description": "One customer's sites. Records come in order of last change, then id. Follow `next_cursor` until it is null to have them all. Query: `limit` (1–200, default 50), `cursor`, `updated_since` (ISO-8601: only records changed at or after it; to sync, pass the time your last sync STARTED, less a minute — a record you already have may come again, at its latest state), and `include_deleted=true` to also get a tombstone `{id, deleted_at, updated_at, version}` for each record in the Trash, so your copy can be deleted too. A record in the Trash, or under a customer in the Trash, is otherwise invisible. Demo (sample) data is never returned. A record permanently deleted from the Trash leaves no tombstone. A key bound to a service account that has been suspended or deleted gets 403 `service_account_required`. Needs the Sites module in use. 404 when the customer is not visible.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "customers:read",
        "x-alty-module": "sites",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "id",
            "in": "path"
          },
          {
            "schema": {
              "type": "string",
              "description": "1–200, default 50."
            },
            "required": false,
            "description": "1–200, default 50.",
            "name": "limit",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "The `next_cursor` of the previous page."
            },
            "required": false,
            "description": "The `next_cursor` of the previous page.",
            "name": "cursor",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "ISO-8601. Only records changed at or after it. To sync, pass the time your last sync STARTED, less a minute."
            },
            "required": false,
            "description": "ISO-8601. Only records changed at or after it. To sync, pass the time your last sync STARTED, less a minute.",
            "name": "updated_since",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ],
              "description": "`true` adds a tombstone `{id, deleted_at, updated_at, version}` for each record in the Trash. A tombstone carries no `external_id`: keep Alty's id beside yours."
            },
            "required": false,
            "description": "`true` adds a tombstone `{id, deleted_at, updated_at, version}` for each record in the Trash. A tombstone carries no `external_id`: keep Alty's id beside yours.",
            "name": "include_deleted",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of sites.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SitePage"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_limit`, `invalid_cursor`, `invalid_updated_since`, `invalid_include_deleted`. A query parameter the endpoint does not list is ignored.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyForbidden"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the same answer whether it does not exist or is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      },
      "post": {
        "tags": [
          "Records"
        ],
        "summary": "Add a site to a customer",
        "description": "Runs exactly what the same change in the app runs — the same checks, history, automatic actions, Xero mirror and live update on open screens — credited to your key’s service account. A field you leave out is left alone; a field sent as null is cleared; an unknown field is 400 `unknown_field`. 404 when the customer is not visible. With `external_id`, a site you already gave that id is updated instead (200).",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "customers:write",
        "x-alty-module": "sites",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "id",
            "in": "path"
          },
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value."
            },
            "required": false,
            "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value.",
            "name": "idempotency-key",
            "in": "header"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SiteCreate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Your external_id already named a site: it was updated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Site"
                }
              }
            }
          },
          "201": {
            "description": "Created: the site.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Site"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_idempotency_key` — the `Idempotency-Key` header is not 1–255 visible ASCII characters. Nothing ran. Also: `invalid_json`, `invalid_field` / `unknown_field` (with `field` and `message`), `invalid_if_match`, or the app’s own refusal in words (an unknown status, a missing name).",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/InvalidIdempotencyKey"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it). Also: `service_account_required`: the key is not bound to an active service account (none, or suspended or deleted).",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "missing_scope"
                          ]
                        },
                        "needs": {
                          "type": "string",
                          "description": "The scope this endpoint needs. Ask your Alty admin for a key that has it."
                        }
                      },
                      "required": [
                        "error",
                        "needs"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "module_off"
                          ]
                        },
                        "module": {
                          "type": "string",
                          "description": "The module that is switched off for this instance, e.g. `support`."
                        }
                      },
                      "required": [
                        "error",
                        "module"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Not found — the same answer whether it does not exist or is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_conflict` — this `Idempotency-Key` was already used with a different request (method, path or body): use a new value for a new request. `idempotency_in_progress` — the first request with this value is still running: wait for `Retry-After` (seconds) and retry with the SAME value. A completed request is never a 409: a retry with the same value and body gets the original status code and body back, with the header `idempotent-replayed: true`. Also: `external_id_taken` (another of your records holds that external id) or `in_trash` (the record holding it is in the Trash: an admin restores it in the app, or deletes it forever to free the id. The API cannot change a record in the Trash).",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/IdempotencyConflict"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/customers/{id}/notes": {
      "get": {
        "tags": [
          "Records"
        ],
        "summary": "List a customer's notes",
        "description": "The notes on one customer, with each author (a service account is `kind: service`). The key needs `notes:read` AND `customers:read`; without the second it gets 403 `missing_scope` naming it. Records come in order of last change, then id. Follow `next_cursor` until it is null to have them all. Query: `limit` (1–200, default 50), `cursor`, `updated_since` (ISO-8601: only records changed at or after it; to sync, pass the time your last sync STARTED, less a minute — a record you already have may come again, at its latest state), and `include_deleted=true` to also get a tombstone `{id, deleted_at, updated_at, version}` for each record in the Trash, so your copy can be deleted too. A record in the Trash, or under a customer in the Trash, is otherwise invisible. Demo (sample) data is never returned. A record permanently deleted from the Trash leaves no tombstone. A key bound to a service account that has been suspended or deleted gets 403 `service_account_required`. 404 when the customer is not visible.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "notes:read",
        "x-alty-module": "core",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "id",
            "in": "path"
          },
          {
            "schema": {
              "type": "string",
              "description": "1–200, default 50."
            },
            "required": false,
            "description": "1–200, default 50.",
            "name": "limit",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "The `next_cursor` of the previous page."
            },
            "required": false,
            "description": "The `next_cursor` of the previous page.",
            "name": "cursor",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "ISO-8601. Only records changed at or after it. To sync, pass the time your last sync STARTED, less a minute."
            },
            "required": false,
            "description": "ISO-8601. Only records changed at or after it. To sync, pass the time your last sync STARTED, less a minute.",
            "name": "updated_since",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ],
              "description": "`true` adds a tombstone `{id, deleted_at, updated_at, version}` for each record in the Trash. A tombstone carries no `external_id`: keep Alty's id beside yours."
            },
            "required": false,
            "description": "`true` adds a tombstone `{id, deleted_at, updated_at, version}` for each record in the Trash. A tombstone carries no `external_id`: keep Alty's id beside yours.",
            "name": "include_deleted",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of notes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotePage"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_limit`, `invalid_cursor`, `invalid_updated_since`, `invalid_include_deleted`. A query parameter the endpoint does not list is ignored.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyForbidden"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the same answer whether it does not exist or is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      },
      "post": {
        "tags": [
          "Records"
        ],
        "summary": "Add a note to a customer",
        "description": "A note on one customer, authored by your key’s service account (shown as an integration). The key needs `notes:write` AND `customers:read`. Notes cannot be changed or deleted through the API. 404 when the customer is not visible.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "notes:write",
        "x-alty-module": "core",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "id",
            "in": "path"
          },
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value."
            },
            "required": false,
            "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value.",
            "name": "idempotency-key",
            "in": "header"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NoteCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created: the note.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Note"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_idempotency_key` — the `Idempotency-Key` header is not 1–255 visible ASCII characters. Nothing ran. Also: `invalid_json`, `invalid_field` / `unknown_field` (with `field` and `message`), `invalid_if_match`, or the app’s own refusal in words (an unknown status, a missing name).",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/InvalidIdempotencyKey"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it). Also: `service_account_required`: the key is not bound to an active service account (none, or suspended or deleted).",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "missing_scope"
                          ]
                        },
                        "needs": {
                          "type": "string",
                          "description": "The scope this endpoint needs. Ask your Alty admin for a key that has it."
                        }
                      },
                      "required": [
                        "error",
                        "needs"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "module_off"
                          ]
                        },
                        "module": {
                          "type": "string",
                          "description": "The module that is switched off for this instance, e.g. `support`."
                        }
                      },
                      "required": [
                        "error",
                        "module"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Not found — the same answer whether it does not exist or is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_conflict` — this `Idempotency-Key` was already used with a different request (method, path or body): use a new value for a new request. `idempotency_in_progress` — the first request with this value is still running: wait for `Retry-After` (seconds) and retry with the SAME value. A completed request is never a 409: a retry with the same value and body gets the original status code and body back, with the header `idempotent-replayed: true`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotencyConflict"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/leads/{id}/notes": {
      "get": {
        "tags": [
          "Records"
        ],
        "summary": "List a lead's notes",
        "description": "The notes on one lead, with each author (a service account is `kind: service`). The key needs `notes:read` AND `leads:read`; without the second it gets 403 `missing_scope` naming it. Records come in order of last change, then id. Follow `next_cursor` until it is null to have them all. Query: `limit` (1–200, default 50), `cursor`, `updated_since` (ISO-8601: only records changed at or after it; to sync, pass the time your last sync STARTED, less a minute — a record you already have may come again, at its latest state), and `include_deleted=true` to also get a tombstone `{id, deleted_at, updated_at, version}` for each record in the Trash, so your copy can be deleted too. A record in the Trash, or under a customer in the Trash, is otherwise invisible. Demo (sample) data is never returned. A record permanently deleted from the Trash leaves no tombstone. A key bound to a service account that has been suspended or deleted gets 403 `service_account_required`. 404 when the lead is not visible.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "notes:read",
        "x-alty-module": "enquiries",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "id",
            "in": "path"
          },
          {
            "schema": {
              "type": "string",
              "description": "1–200, default 50."
            },
            "required": false,
            "description": "1–200, default 50.",
            "name": "limit",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "The `next_cursor` of the previous page."
            },
            "required": false,
            "description": "The `next_cursor` of the previous page.",
            "name": "cursor",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "ISO-8601. Only records changed at or after it. To sync, pass the time your last sync STARTED, less a minute."
            },
            "required": false,
            "description": "ISO-8601. Only records changed at or after it. To sync, pass the time your last sync STARTED, less a minute.",
            "name": "updated_since",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ],
              "description": "`true` adds a tombstone `{id, deleted_at, updated_at, version}` for each record in the Trash. A tombstone carries no `external_id`: keep Alty's id beside yours."
            },
            "required": false,
            "description": "`true` adds a tombstone `{id, deleted_at, updated_at, version}` for each record in the Trash. A tombstone carries no `external_id`: keep Alty's id beside yours.",
            "name": "include_deleted",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of notes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotePage"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_limit`, `invalid_cursor`, `invalid_updated_since`, `invalid_include_deleted`. A query parameter the endpoint does not list is ignored.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyForbidden"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the same answer whether it does not exist or is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      },
      "post": {
        "tags": [
          "Records"
        ],
        "summary": "Add a note to a lead",
        "description": "A note on one lead, authored by your key’s service account (shown as an integration). The key needs `notes:write` AND `leads:read`. Notes cannot be changed or deleted through the API. 404 when the lead is not visible.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "notes:write",
        "x-alty-module": "enquiries",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "id",
            "in": "path"
          },
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value."
            },
            "required": false,
            "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value.",
            "name": "idempotency-key",
            "in": "header"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NoteCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created: the note.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Note"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_idempotency_key` — the `Idempotency-Key` header is not 1–255 visible ASCII characters. Nothing ran. Also: `invalid_json`, `invalid_field` / `unknown_field` (with `field` and `message`), `invalid_if_match`, or the app’s own refusal in words (an unknown status, a missing name).",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/InvalidIdempotencyKey"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it). Also: `service_account_required`: the key is not bound to an active service account (none, or suspended or deleted).",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "missing_scope"
                          ]
                        },
                        "needs": {
                          "type": "string",
                          "description": "The scope this endpoint needs. Ask your Alty admin for a key that has it."
                        }
                      },
                      "required": [
                        "error",
                        "needs"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "module_off"
                          ]
                        },
                        "module": {
                          "type": "string",
                          "description": "The module that is switched off for this instance, e.g. `support`."
                        }
                      },
                      "required": [
                        "error",
                        "module"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Not found — the same answer whether it does not exist or is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_conflict` — this `Idempotency-Key` was already used with a different request (method, path or body): use a new value for a new request. `idempotency_in_progress` — the first request with this value is still running: wait for `Retry-After` (seconds) and retry with the SAME value. A completed request is never a 409: a retry with the same value and body gets the original status code and body back, with the header `idempotent-replayed: true`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotencyConflict"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/projects/{id}/notes": {
      "get": {
        "tags": [
          "Records"
        ],
        "summary": "List a project's notes",
        "description": "The notes on one project, with each author (a service account is `kind: service`). The key needs `notes:read` AND `projects:read`; without the second it gets 403 `missing_scope` naming it. Records come in order of last change, then id. Follow `next_cursor` until it is null to have them all. Query: `limit` (1–200, default 50), `cursor`, `updated_since` (ISO-8601: only records changed at or after it; to sync, pass the time your last sync STARTED, less a minute — a record you already have may come again, at its latest state), and `include_deleted=true` to also get a tombstone `{id, deleted_at, updated_at, version}` for each record in the Trash, so your copy can be deleted too. A record in the Trash, or under a customer in the Trash, is otherwise invisible. Demo (sample) data is never returned. A record permanently deleted from the Trash leaves no tombstone. A key bound to a service account that has been suspended or deleted gets 403 `service_account_required`. 404 when the project is not visible.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "notes:read",
        "x-alty-module": "projects",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "id",
            "in": "path"
          },
          {
            "schema": {
              "type": "string",
              "description": "1–200, default 50."
            },
            "required": false,
            "description": "1–200, default 50.",
            "name": "limit",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "The `next_cursor` of the previous page."
            },
            "required": false,
            "description": "The `next_cursor` of the previous page.",
            "name": "cursor",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "ISO-8601. Only records changed at or after it. To sync, pass the time your last sync STARTED, less a minute."
            },
            "required": false,
            "description": "ISO-8601. Only records changed at or after it. To sync, pass the time your last sync STARTED, less a minute.",
            "name": "updated_since",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ],
              "description": "`true` adds a tombstone `{id, deleted_at, updated_at, version}` for each record in the Trash. A tombstone carries no `external_id`: keep Alty's id beside yours."
            },
            "required": false,
            "description": "`true` adds a tombstone `{id, deleted_at, updated_at, version}` for each record in the Trash. A tombstone carries no `external_id`: keep Alty's id beside yours.",
            "name": "include_deleted",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of notes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotePage"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_limit`, `invalid_cursor`, `invalid_updated_since`, `invalid_include_deleted`. A query parameter the endpoint does not list is ignored.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyForbidden"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the same answer whether it does not exist or is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      },
      "post": {
        "tags": [
          "Records"
        ],
        "summary": "Add a note to a project",
        "description": "A note on one project, authored by your key’s service account (shown as an integration). The key needs `notes:write` AND `projects:read`. Notes cannot be changed or deleted through the API. 404 when the project is not visible.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "notes:write",
        "x-alty-module": "projects",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "id",
            "in": "path"
          },
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value."
            },
            "required": false,
            "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value.",
            "name": "idempotency-key",
            "in": "header"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NoteCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created: the note.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Note"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_idempotency_key` — the `Idempotency-Key` header is not 1–255 visible ASCII characters. Nothing ran. Also: `invalid_json`, `invalid_field` / `unknown_field` (with `field` and `message`), `invalid_if_match`, or the app’s own refusal in words (an unknown status, a missing name).",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/InvalidIdempotencyKey"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it). Also: `service_account_required`: the key is not bound to an active service account (none, or suspended or deleted).",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "missing_scope"
                          ]
                        },
                        "needs": {
                          "type": "string",
                          "description": "The scope this endpoint needs. Ask your Alty admin for a key that has it."
                        }
                      },
                      "required": [
                        "error",
                        "needs"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "module_off"
                          ]
                        },
                        "module": {
                          "type": "string",
                          "description": "The module that is switched off for this instance, e.g. `support`."
                        }
                      },
                      "required": [
                        "error",
                        "module"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Not found — the same answer whether it does not exist or is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_conflict` — this `Idempotency-Key` was already used with a different request (method, path or body): use a new value for a new request. `idempotency_in_progress` — the first request with this value is still running: wait for `Retry-After` (seconds) and retry with the SAME value. A completed request is never a 409: a retry with the same value and body gets the original status code and body back, with the header `idempotent-replayed: true`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotencyConflict"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/members": {
      "get": {
        "tags": [
          "Records"
        ],
        "summary": "List the team",
        "description": "Everyone on the instance — people and service accounts — with the id that `assigned_to` holds. Names, emails and roles only; never sign-in details or preferences. Always one page (`next_cursor` is null).",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "members:read",
        "x-alty-module": "core",
        "responses": {
          "200": {
            "description": "The members.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MemberPage"
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyForbidden"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/customers/{id}/contacts/{cid}": {
      "patch": {
        "tags": [
          "Records"
        ],
        "summary": "Change a customer's contact",
        "description": "Runs exactly what the same change in the app runs — the same checks, history, automatic actions, Xero mirror and live update on open screens — credited to your key’s service account. A field you leave out is left alone; a field sent as null is cleared; an unknown field is 400 `unknown_field`. Send `If-Match: <version>` (the `version` you last read) to refuse overwriting a newer edit: 412 `version_conflict` with `current`, and nothing is written. A PATCH that changes nothing changes nothing: no new version, no history, no Xero push.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "customers:write",
        "x-alty-module": "core",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "id",
            "in": "path"
          },
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "cid",
            "in": "path"
          },
          {
            "schema": {
              "type": "string",
              "description": "Optional: the `version` you last read. If the record has changed since, nothing is written and you get 412 `version_conflict` with `current`. Without it, the last write wins, as in the app."
            },
            "required": false,
            "description": "Optional: the `version` you last read. If the record has changed since, nothing is written and you get 412 `version_conflict` with `current`. Without it, the last write wins, as in the app.",
            "name": "if-match",
            "in": "header"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ContactPatch"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The contact, as it is now.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Contact"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_json`, `invalid_field` / `unknown_field` (with `field` and `message`), `invalid_if_match`, or the app’s own refusal in words (an unknown status, a missing name).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it). Also: `service_account_required`: the key is not bound to an active service account (none, or suspended or deleted).",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "missing_scope"
                          ]
                        },
                        "needs": {
                          "type": "string",
                          "description": "The scope this endpoint needs. Ask your Alty admin for a key that has it."
                        }
                      },
                      "required": [
                        "error",
                        "needs"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "module_off"
                          ]
                        },
                        "module": {
                          "type": "string",
                          "description": "The module that is switched off for this instance, e.g. `support`."
                        }
                      },
                      "required": [
                        "error",
                        "module"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Not found — the same answer whether it does not exist or is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "412": {
            "description": "The record changed since the version in If-Match. Nothing was written.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VersionConflict"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      },
      "delete": {
        "tags": [
          "Records"
        ],
        "summary": "Delete a customer's contact",
        "description": "Removes it outright, as the app does: contacts have no Trash. Takes `If-Match` like PATCH.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "customers:write",
        "x-alty-module": "core",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "id",
            "in": "path"
          },
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "cid",
            "in": "path"
          },
          {
            "schema": {
              "type": "string",
              "description": "Optional: the `version` you last read. If the record has changed since, nothing is written and you get 412 `version_conflict` with `current`. Without it, the last write wins, as in the app."
            },
            "required": false,
            "description": "Optional: the `version` you last read. If the record has changed since, nothing is written and you get 412 `version_conflict` with `current`. Without it, the last write wins, as in the app.",
            "name": "if-match",
            "in": "header"
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Deleted"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_json`, `invalid_field` / `unknown_field` (with `field` and `message`), `invalid_if_match`, or the app’s own refusal in words (an unknown status, a missing name).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it). Also: `service_account_required`: the key is not bound to an active service account (none, or suspended or deleted).",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "missing_scope"
                          ]
                        },
                        "needs": {
                          "type": "string",
                          "description": "The scope this endpoint needs. Ask your Alty admin for a key that has it."
                        }
                      },
                      "required": [
                        "error",
                        "needs"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "module_off"
                          ]
                        },
                        "module": {
                          "type": "string",
                          "description": "The module that is switched off for this instance, e.g. `support`."
                        }
                      },
                      "required": [
                        "error",
                        "module"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Not found — the same answer whether it does not exist or is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "412": {
            "description": "The record changed since the version in If-Match. Nothing was written.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VersionConflict"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/customers/{id}/sites/{cid}": {
      "patch": {
        "tags": [
          "Records"
        ],
        "summary": "Change a customer's site",
        "description": "Runs exactly what the same change in the app runs — the same checks, history, automatic actions, Xero mirror and live update on open screens — credited to your key’s service account. A field you leave out is left alone; a field sent as null is cleared; an unknown field is 400 `unknown_field`. Send `If-Match: <version>` (the `version` you last read) to refuse overwriting a newer edit: 412 `version_conflict` with `current`, and nothing is written. A PATCH that changes nothing changes nothing: no new version, no history, no Xero push.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "customers:write",
        "x-alty-module": "sites",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "id",
            "in": "path"
          },
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "cid",
            "in": "path"
          },
          {
            "schema": {
              "type": "string",
              "description": "Optional: the `version` you last read. If the record has changed since, nothing is written and you get 412 `version_conflict` with `current`. Without it, the last write wins, as in the app."
            },
            "required": false,
            "description": "Optional: the `version` you last read. If the record has changed since, nothing is written and you get 412 `version_conflict` with `current`. Without it, the last write wins, as in the app.",
            "name": "if-match",
            "in": "header"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SitePatch"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The site, as it is now.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Site"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_json`, `invalid_field` / `unknown_field` (with `field` and `message`), `invalid_if_match`, or the app’s own refusal in words (an unknown status, a missing name).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it). Also: `service_account_required`: the key is not bound to an active service account (none, or suspended or deleted).",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "missing_scope"
                          ]
                        },
                        "needs": {
                          "type": "string",
                          "description": "The scope this endpoint needs. Ask your Alty admin for a key that has it."
                        }
                      },
                      "required": [
                        "error",
                        "needs"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "module_off"
                          ]
                        },
                        "module": {
                          "type": "string",
                          "description": "The module that is switched off for this instance, e.g. `support`."
                        }
                      },
                      "required": [
                        "error",
                        "module"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Not found — the same answer whether it does not exist or is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`external_id_taken` (another of your records holds that external id) or `in_trash` (the record holding it is in the Trash: an admin restores it in the app, or deletes it forever to free the id. The API cannot change a record in the Trash).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "412": {
            "description": "The record changed since the version in If-Match. Nothing was written.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VersionConflict"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      },
      "delete": {
        "tags": [
          "Records"
        ],
        "summary": "Delete a customer's site",
        "description": "Moves it to the Trash. Needs the Sites module in use. Takes `If-Match` like PATCH.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "customers:write",
        "x-alty-module": "sites",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "id",
            "in": "path"
          },
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "cid",
            "in": "path"
          },
          {
            "schema": {
              "type": "string",
              "description": "Optional: the `version` you last read. If the record has changed since, nothing is written and you get 412 `version_conflict` with `current`. Without it, the last write wins, as in the app."
            },
            "required": false,
            "description": "Optional: the `version` you last read. If the record has changed since, nothing is written and you get 412 `version_conflict` with `current`. Without it, the last write wins, as in the app.",
            "name": "if-match",
            "in": "header"
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Deleted"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_json`, `invalid_field` / `unknown_field` (with `field` and `message`), `invalid_if_match`, or the app’s own refusal in words (an unknown status, a missing name).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it). Also: `service_account_required`: the key is not bound to an active service account (none, or suspended or deleted).",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "missing_scope"
                          ]
                        },
                        "needs": {
                          "type": "string",
                          "description": "The scope this endpoint needs. Ask your Alty admin for a key that has it."
                        }
                      },
                      "required": [
                        "error",
                        "needs"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "module_off"
                          ]
                        },
                        "module": {
                          "type": "string",
                          "description": "The module that is switched off for this instance, e.g. `support`."
                        }
                      },
                      "required": [
                        "error",
                        "module"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Not found — the same answer whether it does not exist or is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "412": {
            "description": "The record changed since the version in If-Match. Nothing was written.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VersionConflict"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/webhooks/event-types": {
      "get": {
        "tags": [
          "Webhooks"
        ],
        "summary": "List the event types",
        "description": "The published catalogue: each type, when it fires, the summary fields it carries and the read scope it needs.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "webhooks:manage",
        "x-alty-module": "webhooks",
        "responses": {
          "200": {
            "description": "The catalogue.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookEventTypes"
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyForbidden"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/webhooks": {
      "get": {
        "tags": [
          "Webhooks"
        ],
        "summary": "List your webhook subscriptions",
        "description": "A key sees and manages only the subscriptions IT made — not another key’s, even under the same service account, and not an admin’s. A subscription lives and dies with its key: once the key is revoked or expires, it stops receiving and is switched off (a rotated key registers its receiver again). Events are filtered by the key’s scopes as they are at the moment of each change. The secret is never returned.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "webhooks:manage",
        "x-alty-module": "webhooks",
        "responses": {
          "200": {
            "description": "Your subscriptions.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "subscriptions": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WebhookSubscription"
                      }
                    }
                  },
                  "required": [
                    "subscriptions"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyForbidden"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      },
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Subscribe a receiver to events",
        "description": "Registers your receiver and answers with its signing secret — ONCE. Deliveries are signed per Standard Webhooks (`webhook-id`, `webhook-timestamp`, `webhook-signature`). Each event type needs the read scope for its records as well (`customer.*`, `contact.*` and `site.*` need `customers:read`; `lead.*` `leads:read`; `project.*` `projects:read`; `quote.*` `quotes:read`; `invoice.*` `invoices:read`; `note.created` `notes:read`, and a note on a lead or project also needs that resource’s read scope to be sent). Without it: 403 `missing_scope` naming the scope. The URL is checked when saved AND before every delivery: https on port 443, a public host name that resolves only to public addresses, nothing on the Alty domain, no credentials in it. Refused: 400 `unsafe_url` with the reason. Redirects are never followed — a 3xx counts as a failed delivery. At most 20 subscriptions per instance (409 `limit_reached`). Send `POST /webhooks/{id}/test` next to check your receiver end to end.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "webhooks:manage",
        "x-alty-module": "webhooks",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value."
            },
            "required": false,
            "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value.",
            "name": "idempotency-key",
            "in": "header"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Subscribed: the subscription, with `secret` this once.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookSubscriptionWithSecret"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_idempotency_key` — the `Idempotency-Key` header is not 1–255 visible ASCII characters. Nothing ran. Also: `invalid_field` / `unknown_field` (with `field`), `invalid_json`, or `unsafe_url` (with the reason).",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/InvalidIdempotencyKey"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyForbidden"
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_conflict` — this `Idempotency-Key` was already used with a different request (method, path or body): use a new value for a new request. `idempotency_in_progress` — the first request with this value is still running: wait for `Retry-After` (seconds) and retry with the SAME value. A completed request is never a 409: a retry with the same value and body gets the original status code and body back, with the header `idempotent-replayed: true`. Also: `limit_reached`.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/IdempotencyConflict"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/webhooks/{id}": {
      "get": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Get a webhook subscription",
        "description": "A key sees and manages only the subscriptions IT made — not another key’s, even under the same service account, and not an admin’s. A subscription lives and dies with its key: once the key is revoked or expires, it stops receiving and is switched off (a rotated key registers its receiver again). Events are filtered by the key’s scopes as they are at the moment of each change. The secret is never returned.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "webhooks:manage",
        "x-alty-module": "webhooks",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The subscription.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookSubscription"
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyForbidden"
                }
              }
            }
          },
          "404": {
            "description": "No such subscription among this key’s own.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      },
      "patch": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Change a webhook subscription",
        "description": "Change the URL, the event types or the description, or switch it off and on (`enabled`). Switching on starts from NEW events: what happened while it was off is not sent (resend failed deliveries from the log). Each event type needs the read scope for its records as well (`customer.*`, `contact.*` and `site.*` need `customers:read`; `lead.*` `leads:read`; `project.*` `projects:read`; `quote.*` `quotes:read`; `invoice.*` `invoices:read`; `note.created` `notes:read`, and a note on a lead or project also needs that resource’s read scope to be sent). Without it: 403 `missing_scope` naming the scope. The URL is checked when saved AND before every delivery: https on port 443, a public host name that resolves only to public addresses, nothing on the Alty domain, no credentials in it. Refused: 400 `unsafe_url` with the reason. Redirects are never followed — a 3xx counts as a failed delivery.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "webhooks:manage",
        "x-alty-module": "webhooks",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "id",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookPatch"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The subscription, as it is now.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookSubscription"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_field` / `unknown_field` (with `field`), `invalid_json`, or `unsafe_url` (with the reason).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyForbidden"
                }
              }
            }
          },
          "404": {
            "description": "No such subscription among this key’s own.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      },
      "delete": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Delete a webhook subscription",
        "description": "Stops it at once and removes its delivery log.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "webhooks:manage",
        "x-alty-module": "webhooks",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "deleted": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  },
                  "required": [
                    "id",
                    "deleted"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyForbidden"
                }
              }
            }
          },
          "404": {
            "description": "No such subscription among this key’s own.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/webhooks/{id}/test": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Send a test event now",
        "description": "Delivers a signed `webhook.test` to the subscription immediately — not queued, not retried — and answers with what your receiver said and how long it took. Works while the subscription is switched off, so you can check a fix before switching it back on.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "webhooks:manage",
        "x-alty-module": "webhooks",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "id",
            "in": "path"
          },
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value."
            },
            "required": false,
            "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value.",
            "name": "idempotency-key",
            "in": "header"
          }
        ],
        "responses": {
          "200": {
            "description": "The attempt, and your receiver’s answer (`result.ok` is true for a 2xx).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookTestResult"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_idempotency_key` — the `Idempotency-Key` header is not 1–255 visible ASCII characters. Nothing ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvalidIdempotencyKey"
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyForbidden"
                }
              }
            }
          },
          "404": {
            "description": "No such subscription among this key’s own.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_conflict` — this `Idempotency-Key` was already used with a different request (method, path or body): use a new value for a new request. `idempotency_in_progress` — the first request with this value is still running: wait for `Retry-After` (seconds) and retry with the SAME value. A completed request is never a 409: a retry with the same value and body gets the original status code and body back, with the header `idempotent-replayed: true`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotencyConflict"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/webhooks/{id}/rotate-secret": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Rotate the signing secret",
        "description": "Answers with a NEW secret, once. For 24 hours every delivery carries two signatures in `webhook-signature` — one under the new secret, one under the old — so a receiver verifying with either keeps working while you deploy the new one.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "webhooks:manage",
        "x-alty-module": "webhooks",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "id",
            "in": "path"
          },
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value."
            },
            "required": false,
            "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value.",
            "name": "idempotency-key",
            "in": "header"
          }
        ],
        "responses": {
          "200": {
            "description": "The subscription, with the new `secret` this once.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookSubscriptionWithSecret"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_idempotency_key` — the `Idempotency-Key` header is not 1–255 visible ASCII characters. Nothing ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvalidIdempotencyKey"
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyForbidden"
                }
              }
            }
          },
          "404": {
            "description": "No such subscription among this key’s own.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_conflict` — this `Idempotency-Key` was already used with a different request (method, path or body): use a new value for a new request. `idempotency_in_progress` — the first request with this value is still running: wait for `Retry-After` (seconds) and retry with the SAME value. A completed request is never a 409: a retry with the same value and body gets the original status code and body back, with the header `idempotent-replayed: true`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotencyConflict"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/webhooks/{id}/deliveries": {
      "get": {
        "tags": [
          "Webhooks"
        ],
        "summary": "List a subscription’s deliveries",
        "description": "Newest first: each delivery’s event, state, attempts, your last answer’s status and its first 1 KB. Query: `limit` (1–200, default 50), `before` (epoch ms, for the next page). Kept 30 days.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "webhooks:manage",
        "x-alty-module": "webhooks",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The log.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deliveries": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WebhookDelivery"
                      }
                    }
                  },
                  "required": [
                    "deliveries"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyForbidden"
                }
              }
            }
          },
          "404": {
            "description": "No such subscription among this key’s own.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/webhooks/{id}/deliveries/{delivery_id}/resend": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Resend a delivery",
        "description": "Queues the same event again, with the SAME `webhook-id`, to go out within seconds — a receiver that de-duplicates on `webhook-id` handles it safely. The subscription must be switched on (409 `subscription_disabled`).",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "webhooks:manage",
        "x-alty-module": "webhooks",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "id",
            "in": "path"
          },
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "delivery_id",
            "in": "path"
          },
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value."
            },
            "required": false,
            "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value.",
            "name": "idempotency-key",
            "in": "header"
          }
        ],
        "responses": {
          "200": {
            "description": "The new delivery, pending.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookDelivery"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_idempotency_key` — the `Idempotency-Key` header is not 1–255 visible ASCII characters. Nothing ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvalidIdempotencyKey"
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyForbidden"
                }
              }
            }
          },
          "404": {
            "description": "No such subscription among this key’s own.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_conflict` — this `Idempotency-Key` was already used with a different request (method, path or body): use a new value for a new request. `idempotency_in_progress` — the first request with this value is still running: wait for `Retry-After` (seconds) and retry with the SAME value. A completed request is never a 409: a retry with the same value and body gets the original status code and body back, with the header `idempotent-replayed: true`. Also: `subscription_disabled`.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/IdempotencyConflict"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/actions": {
      "get": {
        "tags": [
          "Intelligent Actions"
        ],
        "summary": "List Intelligent Actions",
        "description": "Every action on the instance — switched on or off, buttons and automatic ones — with its whole design (`definition`). The credential is never returned; `has_credential` says whether one is stored. An agent can never switch an action ON, set or read its credential, or test-run it: those are an admin’s steps in the app (Admin › Intelligent Actions), and there is no route for them.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "actions:read",
        "x-alty-module": "actions",
        "responses": {
          "200": {
            "description": "The actions.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ActionList"
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyForbidden"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      },
      "post": {
        "tags": [
          "Intelligent Actions"
        ],
        "summary": "Draft a new action",
        "description": "Saves a new action SWITCHED OFF, marked as drafted by this key’s service account, for an admin to review, give its credential and switch on. A design with a `trigger` is saved unarmed. `why` is shown to the admin on the draft. An agent can never switch an action ON, set or read its credential, or test-run it: those are an admin’s steps in the app (Admin › Intelligent Actions), and there is no route for them. The key must be bound to an active service account.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "actions:write",
        "x-alty-module": "actions",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value."
            },
            "required": false,
            "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value.",
            "name": "idempotency-key",
            "in": "header"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ActionWrite"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The draft.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ActionSaved"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_idempotency_key` — the `Idempotency-Key` header is not 1–255 visible ASCII characters. Nothing ran. Also: `invalid_field` / `unknown_field` (with `field`), `invalid_json`, or `invalid_action` with `errors`: each field the design gets wrong, as the designer shows them.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/InvalidIdempotencyKey"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyForbidden"
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_conflict` — this `Idempotency-Key` was already used with a different request (method, path or body): use a new value for a new request. `idempotency_in_progress` — the first request with this value is still running: wait for `Retry-After` (seconds) and retry with the SAME value. A completed request is never a 409: a retry with the same value and body gets the original status code and body back, with the header `idempotent-replayed: true`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotencyConflict"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/actions/runs": {
      "get": {
        "tags": [
          "Intelligent Actions"
        ],
        "summary": "Read the run log",
        "description": "Newest first. Query: `action_id` (one action’s history), or `record_type` + `record_id` (what was run on one record); `limit` up to 200 (default 50). Each run shows exactly what was sent, with the credential header `[redacted]`, and what came back. It returns the record’s data, so the key ALSO needs that record’s read scope: `leads:read` for an action on leads (`enquiry`), `customers:read` on customers, `projects:read` on projects. Without it: 403 `missing_scope` naming the scope.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "actions:read",
        "x-alty-module": "actions",
        "responses": {
          "200": {
            "description": "The runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ActionRunList"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_field` / `unknown_field` (with `field`), `invalid_json`, or `invalid_action` with `errors`: each field the design gets wrong, as the designer shows them.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it). Also: `missing_scope` (with `needs`): the record’s read scope is also required.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "missing_scope"
                          ]
                        },
                        "needs": {
                          "type": "string",
                          "description": "The scope this endpoint needs. Ask your Alty admin for a key that has it."
                        }
                      },
                      "required": [
                        "error",
                        "needs"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "module_off"
                          ]
                        },
                        "module": {
                          "type": "string",
                          "description": "The module that is switched off for this instance, e.g. `support`."
                        }
                      },
                      "required": [
                        "error",
                        "module"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "missing_scope"
                          ]
                        },
                        "needs": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        }
                      },
                      "required": [
                        "error",
                        "needs",
                        "message"
                      ]
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "No such action.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/actions/{id}": {
      "get": {
        "tags": [
          "Intelligent Actions"
        ],
        "summary": "Read an action",
        "description": "One action and its whole design. The credential is never returned.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "actions:read",
        "x-alty-module": "actions",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The action.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Action"
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyForbidden"
                }
              }
            }
          },
          "404": {
            "description": "No such action.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      },
      "put": {
        "tags": [
          "Intelligent Actions"
        ],
        "summary": "Change a switched-off action",
        "description": "Replaces the design of an action that is switched OFF; it stays off. An action that is switched on is refused (409): switch it off first with `POST /actions/{id}/disable`. If the change alters what the credential is used for — the method, URL, headers or body, the allowed hosts, auth, poll, inputs, who may run it, its trigger, record type or the action it runs next — the stored credential is REMOVED (`credential_dropped: true`), so an admin re-enters it after checking the new destination. An agent can never switch an action ON, set or read its credential, or test-run it: those are an admin’s steps in the app (Admin › Intelligent Actions), and there is no route for them.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "actions:write",
        "x-alty-module": "actions",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "id",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ActionWrite"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The action, as it is now.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ActionSaved"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_field` / `unknown_field` (with `field`), `invalid_json`, or `invalid_action` with `errors`: each field the design gets wrong, as the designer shows them.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyForbidden"
                }
              }
            }
          },
          "404": {
            "description": "No such action.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`conflict`: the action is switched on.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "message": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error",
                    "message"
                  ]
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      },
      "delete": {
        "tags": [
          "Intelligent Actions"
        ],
        "summary": "Delete your own unused draft",
        "description": "Deletes an action only when THIS key’s service account drafted it, it is switched off, it has never run (an admin’s test run counts), and no other action runs it next. Anything else is 409 with the reason: an admin deletes it in the app. An admin who saves over a draft in the designer adopts it, and it is theirs from then on.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "actions:write",
        "x-alty-module": "actions",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "deleted": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  },
                  "required": [
                    "id",
                    "deleted"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyForbidden"
                }
              }
            }
          },
          "404": {
            "description": "No such action.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`conflict`, with the reason it cannot be deleted by an agent.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "message": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error",
                    "message"
                  ]
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/actions/{id}/preview": {
      "post": {
        "tags": [
          "Intelligent Actions"
        ],
        "summary": "Preview a run on a record",
        "description": "Renders the request the action WOULD send for this record and these inputs — method, URL, headers and body — and sends nothing. The credential’s headers are named with the value `[redacted]`. It returns the record’s data, so the key ALSO needs that record’s read scope: `leads:read` for an action on leads (`enquiry`), `customers:read` on customers, `projects:read` on projects. Without it: 403 `missing_scope` naming the scope.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "actions:read",
        "x-alty-module": "actions",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "id",
            "in": "path"
          },
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value."
            },
            "required": false,
            "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value.",
            "name": "idempotency-key",
            "in": "header"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ActionRunRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The request it would send.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ActionPreview"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_idempotency_key` — the `Idempotency-Key` header is not 1–255 visible ASCII characters. Nothing ran. Also: `invalid_field` / `unknown_field` (with `field`), `invalid_json`, or `invalid_action` with `errors`: each field the design gets wrong, as the designer shows them.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/InvalidIdempotencyKey"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it). Also: `missing_scope` (with `needs`): the record’s read scope is also required.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "missing_scope"
                          ]
                        },
                        "needs": {
                          "type": "string",
                          "description": "The scope this endpoint needs. Ask your Alty admin for a key that has it."
                        }
                      },
                      "required": [
                        "error",
                        "needs"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "module_off"
                          ]
                        },
                        "module": {
                          "type": "string",
                          "description": "The module that is switched off for this instance, e.g. `support`."
                        }
                      },
                      "required": [
                        "error",
                        "module"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "missing_scope"
                          ]
                        },
                        "needs": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        }
                      },
                      "required": [
                        "error",
                        "needs",
                        "message"
                      ]
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "No such action.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_conflict` — this `Idempotency-Key` was already used with a different request (method, path or body): use a new value for a new request. `idempotency_in_progress` — the first request with this value is still running: wait for `Retry-After` (seconds) and retry with the SAME value. A completed request is never a 409: a retry with the same value and body gets the original status code and body back, with the header `idempotent-replayed: true`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotencyConflict"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/actions/{id}/run": {
      "post": {
        "tags": [
          "Intelligent Actions"
        ],
        "summary": "Run an action on a record",
        "description": "Runs an action an admin has switched ON, on one record, exactly as the record’s button does: the request is sent with the stored credential, then its steps (fields, note, status, …) run if it succeeded. The key runs as its service account, which is `staff`: an action only admins may run is refused (403), as is one that is switched off or automatic (409). Every run is in the log under the service account. A key may make at most 100 runs in any 24 hours (429 `run_cap_reached` with `resets_at`). It returns the record’s data, so the key ALSO needs that record’s read scope: `leads:read` for an action on leads (`enquiry`), `customers:read` on customers, `projects:read` on projects. Without it: 403 `missing_scope` naming the scope. The key must be bound to an active service account.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "actions:run",
        "x-alty-module": "actions",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "id",
            "in": "path"
          },
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value."
            },
            "required": false,
            "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value.",
            "name": "idempotency-key",
            "in": "header"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ActionRunRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The run, finished (or `waiting`, for an action that polls until done).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ActionRun"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_idempotency_key` — the `Idempotency-Key` header is not 1–255 visible ASCII characters. Nothing ran. Also: `invalid_field` / `unknown_field` (with `field`), `invalid_json`, or `invalid_action` with `errors`: each field the design gets wrong, as the designer shows them.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/InvalidIdempotencyKey"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it). Also: `missing_scope` (the record’s read scope), or `forbidden`: only admins may run this action.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "missing_scope"
                          ]
                        },
                        "needs": {
                          "type": "string",
                          "description": "The scope this endpoint needs. Ask your Alty admin for a key that has it."
                        }
                      },
                      "required": [
                        "error",
                        "needs"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "module_off"
                          ]
                        },
                        "module": {
                          "type": "string",
                          "description": "The module that is switched off for this instance, e.g. `support`."
                        }
                      },
                      "required": [
                        "error",
                        "module"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "missing_scope"
                          ]
                        },
                        "needs": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        }
                      },
                      "required": [
                        "error",
                        "needs",
                        "message"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        }
                      },
                      "required": [
                        "error",
                        "message"
                      ]
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "No such action.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_conflict` — this `Idempotency-Key` was already used with a different request (method, path or body): use a new value for a new request. `idempotency_in_progress` — the first request with this value is still running: wait for `Retry-After` (seconds) and retry with the SAME value. A completed request is never a 409: a retry with the same value and body gets the original status code and body back, with the header `idempotent-replayed: true`. Also: `conflict`: the action is switched off, runs automatically, or has no credential yet.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/IdempotencyConflict"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        }
                      },
                      "required": [
                        "error",
                        "message"
                      ]
                    }
                  ]
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs. Also: `run_cap_reached`: 100 runs in the last 24 hours. `resets_at` says when one is free.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/KeyRateLimited"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "run_cap_reached"
                          ]
                        },
                        "resets_at": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        }
                      },
                      "required": [
                        "error",
                        "resets_at",
                        "message"
                      ]
                    }
                  ]
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/actions/{id}/disable": {
      "post": {
        "tags": [
          "Intelligent Actions"
        ],
        "summary": "Switch an action off",
        "description": "Switches an action off — it stops being offered and stops running on events — and records which service account did it, for the admin who switches it back on. Already off: answered as it is. There is no route to switch an action on.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "actions:write",
        "x-alty-module": "actions",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "id",
            "in": "path"
          },
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value."
            },
            "required": false,
            "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value.",
            "name": "idempotency-key",
            "in": "header"
          }
        ],
        "responses": {
          "200": {
            "description": "The action, now off.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Action"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_idempotency_key` — the `Idempotency-Key` header is not 1–255 visible ASCII characters. Nothing ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvalidIdempotencyKey"
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyForbidden"
                }
              }
            }
          },
          "404": {
            "description": "No such action.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_conflict` — this `Idempotency-Key` was already used with a different request (method, path or body): use a new value for a new request. `idempotency_in_progress` — the first request with this value is still running: wait for `Retry-After` (seconds) and retry with the SAME value. A completed request is never a 409: a retry with the same value and body gets the original status code and body back, with the header `idempotent-replayed: true`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotencyConflict"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/support/tickets": {
      "post": {
        "tags": [
          "Support desk — requester"
        ],
        "summary": "Raise a ticket for a requester",
        "description": "Server-to-server only: the caller asserts the requester. A repeat with the same `message_id` returns the existing ticket (200, `duplicate: true`). Files are stored after the ticket, so a refused file never loses the words.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "support:requester",
        "x-alty-module": "support",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value."
            },
            "required": false,
            "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value.",
            "name": "idempotency-key",
            "in": "header"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "requester_email": {
                    "type": "string"
                  },
                  "requester_name": {
                    "type": "string"
                  },
                  "subject": {
                    "type": "string"
                  },
                  "body": {
                    "type": "string"
                  },
                  "type": {
                    "type": "string",
                    "description": "A TICKET_TYPE picklist key."
                  },
                  "source_ref": {
                    "type": "string"
                  },
                  "message_id": {
                    "type": "string",
                    "description": "Your id for this message; makes the call safe to retry."
                  },
                  "in_reply_to": {
                    "type": "string"
                  },
                  "customer_external_ref": {
                    "type": "string",
                    "description": "Link to a desk customer by your own id for it."
                  },
                  "customer_name": {
                    "type": "string"
                  },
                  "attachments": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/AttachmentIn"
                    }
                  }
                },
                "required": [
                  "requester_email",
                  "subject",
                  "body"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Threaded onto an existing ticket, or a duplicate of an earlier call.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "reference": {
                      "type": "string"
                    },
                    "is_new": {
                      "type": "boolean"
                    },
                    "duplicate": {
                      "type": "boolean"
                    },
                    "attachments": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Attachment"
                      }
                    },
                    "attachment_failures": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Each file that did not store, and why. The words were saved regardless."
                    }
                  },
                  "required": [
                    "reference",
                    "is_new",
                    "duplicate"
                  ]
                }
              }
            }
          },
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "reference": {
                      "type": "string"
                    },
                    "is_new": {
                      "type": "boolean"
                    },
                    "duplicate": {
                      "type": "boolean"
                    },
                    "attachments": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Attachment"
                      }
                    },
                    "attachment_failures": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Each file that did not store, and why. The words were saved regardless."
                    }
                  },
                  "required": [
                    "reference",
                    "is_new",
                    "duplicate"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`invalid_idempotency_key` — the `Idempotency-Key` header is not 1–255 visible ASCII characters. Nothing ran. Also: The request body is invalid; `error` says which field.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/InvalidIdempotencyKey"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyForbidden"
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_conflict` — this `Idempotency-Key` was already used with a different request (method, path or body): use a new value for a new request. `idempotency_in_progress` — the first request with this value is still running: wait for `Retry-After` (seconds) and retry with the SAME value. A completed request is never a 409: a retry with the same value and body gets the original status code and body back, with the header `idempotent-replayed: true`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotencyConflict"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs. Also: Too many attachments uploaded to this desk recently.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/KeyRateLimited"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/support/tickets/lookup": {
      "post": {
        "tags": [
          "Support desk — requester"
        ],
        "summary": "Read one of the requester's tickets, or list them",
        "description": "POST so email addresses stay out of URLs. With `reference`: that ticket (requester view — no internal notes). Without: a page plus `total`, the count for the filter regardless of `limit` (clamped 1–300).",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "support:requester",
        "x-alty-module": "support",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value."
            },
            "required": false,
            "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value.",
            "name": "idempotency-key",
            "in": "header"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "source_ref": {
                    "type": "string",
                    "description": "Which product or instance the requester belongs to. Every read is filtered by it. A key with a source_ref hint may only assert that hint or `<hint>:<anything>`."
                  },
                  "requester_email": {
                    "type": "string",
                    "description": "Who the requester is — from YOUR authenticated session, never from a browser."
                  },
                  "reference": {
                    "type": "string"
                  },
                  "filter": {
                    "type": "string",
                    "enum": [
                      "open",
                      "needs_me",
                      "all"
                    ],
                    "description": "Default open. needs_me = waiting on the requester."
                  },
                  "limit": {
                    "type": "number"
                  }
                },
                "required": [
                  "source_ref"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "`{ticket}` for a reference, else `{tickets, total}`.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "ticket": {
                          "$ref": "#/components/schemas/Ticket"
                        }
                      },
                      "required": [
                        "ticket"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "tickets": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Ticket"
                          }
                        },
                        "total": {
                          "type": "number"
                        }
                      },
                      "required": [
                        "tickets",
                        "total"
                      ]
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`invalid_idempotency_key` — the `Idempotency-Key` header is not 1–255 visible ASCII characters. Nothing ran. Also: The request body is invalid; `error` says which field.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/InvalidIdempotencyKey"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyForbidden"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the same answer whether it does not exist or is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_conflict` — this `Idempotency-Key` was already used with a different request (method, path or body): use a new value for a new request. `idempotency_in_progress` — the first request with this value is still running: wait for `Retry-After` (seconds) and retry with the SAME value. A completed request is never a 409: a retry with the same value and body gets the original status code and body back, with the header `idempotent-replayed: true`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotencyConflict"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/support/tickets/reply": {
      "post": {
        "tags": [
          "Support desk — requester"
        ],
        "summary": "Reply as the requester",
        "description": "A retry carrying the same `message_id` posts nothing and returns `duplicate: true`. Replying reopens a ticket waiting on the requester.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "support:requester",
        "x-alty-module": "support",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value."
            },
            "required": false,
            "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value.",
            "name": "idempotency-key",
            "in": "header"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "source_ref": {
                    "type": "string",
                    "description": "Which product or instance the requester belongs to. Every read is filtered by it. A key with a source_ref hint may only assert that hint or `<hint>:<anything>`."
                  },
                  "requester_email": {
                    "type": "string",
                    "description": "Who the requester is — from YOUR authenticated session, never from a browser."
                  },
                  "reference": {
                    "type": "string"
                  },
                  "message": {
                    "type": "string"
                  },
                  "message_id": {
                    "type": "string"
                  },
                  "attachments": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/AttachmentIn"
                    }
                  }
                },
                "required": [
                  "source_ref",
                  "reference",
                  "message"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The ticket after the reply.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ticket": {
                      "$ref": "#/components/schemas/Ticket"
                    },
                    "duplicate": {
                      "type": "boolean"
                    },
                    "attachments": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Attachment"
                      }
                    },
                    "attachment_failures": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Each file that did not store, and why. The words were saved regardless."
                    }
                  },
                  "required": [
                    "ticket"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`invalid_idempotency_key` — the `Idempotency-Key` header is not 1–255 visible ASCII characters. Nothing ran. Also: The request body is invalid; `error` says which field.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/InvalidIdempotencyKey"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyForbidden"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the same answer whether it does not exist or is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_conflict` — this `Idempotency-Key` was already used with a different request (method, path or body): use a new value for a new request. `idempotency_in_progress` — the first request with this value is still running: wait for `Retry-After` (seconds) and retry with the SAME value. A completed request is never a 409: a retry with the same value and body gets the original status code and body back, with the header `idempotent-replayed: true`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotencyConflict"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs. Also: Too many attachments uploaded to this desk recently.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/KeyRateLimited"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/support/attachments/get": {
      "post": {
        "tags": [
          "Support desk — requester"
        ],
        "summary": "Download a file from the requester's own ticket",
        "description": "Never a file from an internal note. One 404 for every refusal.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "support:requester",
        "x-alty-module": "support",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value."
            },
            "required": false,
            "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value.",
            "name": "idempotency-key",
            "in": "header"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "source_ref": {
                    "type": "string",
                    "description": "Which product or instance the requester belongs to. Every read is filtered by it. A key with a source_ref hint may only assert that hint or `<hint>:<anything>`."
                  },
                  "requester_email": {
                    "type": "string",
                    "description": "Who the requester is — from YOUR authenticated session, never from a browser."
                  },
                  "reference": {
                    "type": "string"
                  },
                  "attachment_id": {
                    "type": "string"
                  }
                },
                "required": [
                  "source_ref",
                  "reference",
                  "attachment_id"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The file, with download headers and `nosniff`.",
            "content": {
              "application/octet-stream": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_idempotency_key` — the `Idempotency-Key` header is not 1–255 visible ASCII characters. Nothing ran. Also: The request body is invalid; `error` says which field.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/InvalidIdempotencyKey"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyForbidden"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the same answer whether it does not exist or is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_conflict` — this `Idempotency-Key` was already used with a different request (method, path or body): use a new value for a new request. `idempotency_in_progress` — the first request with this value is still running: wait for `Retry-After` (seconds) and retry with the SAME value. A completed request is never a 409: a retry with the same value and body gets the original status code and body back, with the header `idempotent-replayed: true`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotencyConflict"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "503": {
            "description": "A dependency (file storage, PDF service) is unavailable; retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/support/tickets/close": {
      "post": {
        "tags": [
          "Support desk — requester"
        ],
        "summary": "Close a ticket as the requester",
        "description": "Outcome defaults to resolved.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "support:requester",
        "x-alty-module": "support",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value."
            },
            "required": false,
            "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value.",
            "name": "idempotency-key",
            "in": "header"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "source_ref": {
                    "type": "string",
                    "description": "Which product or instance the requester belongs to. Every read is filtered by it. A key with a source_ref hint may only assert that hint or `<hint>:<anything>`."
                  },
                  "requester_email": {
                    "type": "string",
                    "description": "Who the requester is — from YOUR authenticated session, never from a browser."
                  },
                  "reference": {
                    "type": "string"
                  },
                  "outcome": {
                    "type": "string",
                    "enum": [
                      "resolved",
                      "cancelled"
                    ]
                  },
                  "comment": {
                    "type": "string"
                  }
                },
                "required": [
                  "source_ref",
                  "reference"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The closed ticket.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ticket": {
                      "$ref": "#/components/schemas/Ticket"
                    }
                  },
                  "required": [
                    "ticket"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`invalid_idempotency_key` — the `Idempotency-Key` header is not 1–255 visible ASCII characters. Nothing ran. Also: The request body is invalid; `error` says which field.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/InvalidIdempotencyKey"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyForbidden"
                }
              }
            }
          },
          "404": {
            "description": "Not found — the same answer whether it does not exist or is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_conflict` — this `Idempotency-Key` was already used with a different request (method, path or body): use a new value for a new request. `idempotency_in_progress` — the first request with this value is still running: wait for `Retry-After` (seconds) and retry with the SAME value. A completed request is never a 409: a retry with the same value and body gets the original status code and body back, with the header `idempotent-replayed: true`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotencyConflict"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/support/agent/tickets/list": {
      "post": {
        "tags": [
          "Support desk — agent"
        ],
        "summary": "List the desk queue",
        "description": "Desk-wide, staff view. `total` is the count for the filter regardless of paging.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "support:agent",
        "x-alty-module": "support",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value."
            },
            "required": false,
            "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value.",
            "name": "idempotency-key",
            "in": "header"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "statuses": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "filter": {
                    "type": "string",
                    "enum": [
                      "open",
                      "closed",
                      "all"
                    ]
                  },
                  "requester_emails": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "assignee": {
                    "type": "string"
                  },
                  "unassigned": {
                    "type": "boolean"
                  },
                  "limit": {
                    "type": "number"
                  },
                  "offset": {
                    "type": "number"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A page of tickets.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "tickets": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Ticket"
                      }
                    },
                    "total": {
                      "type": "number"
                    }
                  },
                  "required": [
                    "tickets",
                    "total"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`invalid_idempotency_key` — the `Idempotency-Key` header is not 1–255 visible ASCII characters. Nothing ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvalidIdempotencyKey"
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it). Also: The key is not bound to an active service account; agent actions must be attributable.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "missing_scope"
                          ]
                        },
                        "needs": {
                          "type": "string",
                          "description": "The scope this endpoint needs. Ask your Alty admin for a key that has it."
                        }
                      },
                      "required": [
                        "error",
                        "needs"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "module_off"
                          ]
                        },
                        "module": {
                          "type": "string",
                          "description": "The module that is switched off for this instance, e.g. `support`."
                        }
                      },
                      "required": [
                        "error",
                        "module"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_conflict` — this `Idempotency-Key` was already used with a different request (method, path or body): use a new value for a new request. `idempotency_in_progress` — the first request with this value is still running: wait for `Retry-After` (seconds) and retry with the SAME value. A completed request is never a 409: a retry with the same value and body gets the original status code and body back, with the header `idempotent-replayed: true`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotencyConflict"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/support/agent/tickets/get": {
      "post": {
        "tags": [
          "Support desk — agent"
        ],
        "summary": "Read a ticket (staff view)",
        "description": "The full thread including internal notes, the audit trail and links.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "support:agent",
        "x-alty-module": "support",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value."
            },
            "required": false,
            "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value.",
            "name": "idempotency-key",
            "in": "header"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reference": {
                    "type": "string"
                  }
                },
                "required": [
                  "reference"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The ticket.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ticket": {
                      "$ref": "#/components/schemas/Ticket"
                    }
                  },
                  "required": [
                    "ticket"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`invalid_idempotency_key` — the `Idempotency-Key` header is not 1–255 visible ASCII characters. Nothing ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvalidIdempotencyKey"
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it). Also: The key is not bound to an active service account; agent actions must be attributable.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "missing_scope"
                          ]
                        },
                        "needs": {
                          "type": "string",
                          "description": "The scope this endpoint needs. Ask your Alty admin for a key that has it."
                        }
                      },
                      "required": [
                        "error",
                        "needs"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "module_off"
                          ]
                        },
                        "module": {
                          "type": "string",
                          "description": "The module that is switched off for this instance, e.g. `support`."
                        }
                      },
                      "required": [
                        "error",
                        "module"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Not found — the same answer whether it does not exist or is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_conflict` — this `Idempotency-Key` was already used with a different request (method, path or body): use a new value for a new request. `idempotency_in_progress` — the first request with this value is still running: wait for `Retry-After` (seconds) and retry with the SAME value. A completed request is never a 409: a retry with the same value and body gets the original status code and body back, with the header `idempotent-replayed: true`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotencyConflict"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/support/agent/tickets/status": {
      "post": {
        "tags": [
          "Support desk — agent"
        ],
        "summary": "Change a ticket's status",
        "description": "Any TICKET_STATUS key; the same rules as the console (cancel needs a reason, reopen rules). Emails the chosen recipients when the email channel is on, unless `send_email: false`.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "support:agent",
        "x-alty-module": "support",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value."
            },
            "required": false,
            "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value.",
            "name": "idempotency-key",
            "in": "header"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reference": {
                    "type": "string"
                  },
                  "status": {
                    "type": "string"
                  },
                  "message": {
                    "type": "string"
                  },
                  "send_email": {
                    "type": "boolean"
                  },
                  "closed_reason": {
                    "type": "string"
                  },
                  "recipients": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Email addresses already on the ticket."
                  }
                },
                "required": [
                  "reference",
                  "status"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated ticket and who was emailed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ticket": {
                      "$ref": "#/components/schemas/Ticket"
                    },
                    "email_sent": {
                      "type": "boolean"
                    },
                    "recipients": {
                      "type": "array",
                      "items": {}
                    }
                  },
                  "required": [
                    "ticket",
                    "email_sent",
                    "recipients"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`invalid_idempotency_key` — the `Idempotency-Key` header is not 1–255 visible ASCII characters. Nothing ran. Also: The request body is invalid; `error` says which field.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/InvalidIdempotencyKey"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it). Also: The key is not bound to an active service account; agent actions must be attributable.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "missing_scope"
                          ]
                        },
                        "needs": {
                          "type": "string",
                          "description": "The scope this endpoint needs. Ask your Alty admin for a key that has it."
                        }
                      },
                      "required": [
                        "error",
                        "needs"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "module_off"
                          ]
                        },
                        "module": {
                          "type": "string",
                          "description": "The module that is switched off for this instance, e.g. `support`."
                        }
                      },
                      "required": [
                        "error",
                        "module"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Not found — the same answer whether it does not exist or is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_conflict` — this `Idempotency-Key` was already used with a different request (method, path or body): use a new value for a new request. `idempotency_in_progress` — the first request with this value is still running: wait for `Retry-After` (seconds) and retry with the SAME value. A completed request is never a 409: a retry with the same value and body gets the original status code and body back, with the header `idempotent-replayed: true`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotencyConflict"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/support/agent/tickets/reply": {
      "post": {
        "tags": [
          "Support desk — agent"
        ],
        "summary": "Reply on a ticket",
        "description": "Email is opt-in here (`send_email: true`). Unsent, the reply is still recorded on the thread.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "support:agent",
        "x-alty-module": "support",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value."
            },
            "required": false,
            "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value.",
            "name": "idempotency-key",
            "in": "header"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reference": {
                    "type": "string"
                  },
                  "body": {
                    "type": "string"
                  },
                  "to": {
                    "type": "string",
                    "enum": [
                      "requester",
                      "contacts"
                    ]
                  },
                  "send_email": {
                    "type": "boolean"
                  }
                },
                "required": [
                  "reference",
                  "body"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The ticket after the reply.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ticket": {
                      "$ref": "#/components/schemas/Ticket"
                    },
                    "email_sent": {
                      "type": "boolean"
                    },
                    "recipients": {
                      "type": "array",
                      "items": {}
                    }
                  },
                  "required": [
                    "ticket",
                    "email_sent"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`invalid_idempotency_key` — the `Idempotency-Key` header is not 1–255 visible ASCII characters. Nothing ran. Also: The request body is invalid; `error` says which field.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/InvalidIdempotencyKey"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it). Also: The key is not bound to an active service account; agent actions must be attributable.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "missing_scope"
                          ]
                        },
                        "needs": {
                          "type": "string",
                          "description": "The scope this endpoint needs. Ask your Alty admin for a key that has it."
                        }
                      },
                      "required": [
                        "error",
                        "needs"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "module_off"
                          ]
                        },
                        "module": {
                          "type": "string",
                          "description": "The module that is switched off for this instance, e.g. `support`."
                        }
                      },
                      "required": [
                        "error",
                        "module"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Not found — the same answer whether it does not exist or is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_conflict` — this `Idempotency-Key` was already used with a different request (method, path or body): use a new value for a new request. `idempotency_in_progress` — the first request with this value is still running: wait for `Retry-After` (seconds) and retry with the SAME value. A completed request is never a 409: a retry with the same value and body gets the original status code and body back, with the header `idempotent-replayed: true`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotencyConflict"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/support/agent/tickets/reply/preview": {
      "post": {
        "tags": [
          "Support desk — agent"
        ],
        "summary": "Preview a reply before sending it",
        "description": "Exactly what `tickets/reply` would email: the envelope and the mail, built by the same code. Nothing is written to the thread and nothing is sent. A reply to contacts is one email with all of them on its To line.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "support:agent",
        "x-alty-module": "support",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value."
            },
            "required": false,
            "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value.",
            "name": "idempotency-key",
            "in": "header"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reference": {
                    "type": "string"
                  },
                  "body": {
                    "type": "string"
                  },
                  "to": {
                    "type": "string",
                    "enum": [
                      "requester",
                      "contacts"
                    ]
                  },
                  "format": {
                    "type": "string",
                    "enum": [
                      "markdown",
                      "text"
                    ]
                  }
                },
                "required": [
                  "reference",
                  "body"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The email as it would be sent.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "preview": {
                      "type": "object",
                      "properties": {
                        "from": {
                          "type": "string"
                        },
                        "to": {
                          "type": "string"
                        },
                        "reply_to": {
                          "type": "string"
                        },
                        "subject": {
                          "type": "string"
                        },
                        "html": {
                          "type": "string"
                        },
                        "text": {
                          "type": "string"
                        },
                        "attachments": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      },
                      "required": [
                        "from",
                        "to",
                        "reply_to",
                        "subject",
                        "html",
                        "text",
                        "attachments"
                      ]
                    }
                  },
                  "required": [
                    "preview"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`invalid_idempotency_key` — the `Idempotency-Key` header is not 1–255 visible ASCII characters. Nothing ran. Also: The request body is invalid; `error` says which field.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/InvalidIdempotencyKey"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it). Also: The key is not bound to an active service account; agent actions must be attributable.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "missing_scope"
                          ]
                        },
                        "needs": {
                          "type": "string",
                          "description": "The scope this endpoint needs. Ask your Alty admin for a key that has it."
                        }
                      },
                      "required": [
                        "error",
                        "needs"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "module_off"
                          ]
                        },
                        "module": {
                          "type": "string",
                          "description": "The module that is switched off for this instance, e.g. `support`."
                        }
                      },
                      "required": [
                        "error",
                        "module"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Not found — the same answer whether it does not exist or is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_conflict` — this `Idempotency-Key` was already used with a different request (method, path or body): use a new value for a new request. `idempotency_in_progress` — the first request with this value is still running: wait for `Retry-After` (seconds) and retry with the SAME value. A completed request is never a 409: a retry with the same value and body gets the original status code and body back, with the header `idempotent-replayed: true`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotencyConflict"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/support/agent/tickets/contacts": {
      "post": {
        "tags": [
          "Support desk — agent"
        ],
        "summary": "Add or remove a ticket's contacts",
        "description": "Contacts are the ticket customer's own contacts; a new person becomes a contact on that customer.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "support:agent",
        "x-alty-module": "support",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value."
            },
            "required": false,
            "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value.",
            "name": "idempotency-key",
            "in": "header"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reference": {
                    "type": "string"
                  },
                  "add": {
                    "type": "array",
                    "items": {
                      "anyOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "name": {
                              "type": "string"
                            },
                            "email": {
                              "type": "string"
                            },
                            "role": {
                              "type": "string"
                            }
                          },
                          "required": [
                            "name",
                            "email"
                          ]
                        }
                      ]
                    }
                  },
                  "remove": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                },
                "required": [
                  "reference"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The ticket.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ticket": {
                      "$ref": "#/components/schemas/Ticket"
                    }
                  },
                  "required": [
                    "ticket"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`invalid_idempotency_key` — the `Idempotency-Key` header is not 1–255 visible ASCII characters. Nothing ran. Also: The request body is invalid; `error` says which field.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/InvalidIdempotencyKey"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it). Also: The key is not bound to an active service account; agent actions must be attributable.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "missing_scope"
                          ]
                        },
                        "needs": {
                          "type": "string",
                          "description": "The scope this endpoint needs. Ask your Alty admin for a key that has it."
                        }
                      },
                      "required": [
                        "error",
                        "needs"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "module_off"
                          ]
                        },
                        "module": {
                          "type": "string",
                          "description": "The module that is switched off for this instance, e.g. `support`."
                        }
                      },
                      "required": [
                        "error",
                        "module"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Not found — the same answer whether it does not exist or is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_conflict` — this `Idempotency-Key` was already used with a different request (method, path or body): use a new value for a new request. `idempotency_in_progress` — the first request with this value is still running: wait for `Retry-After` (seconds) and retry with the SAME value. A completed request is never a 409: a retry with the same value and body gets the original status code and body back, with the header `idempotent-replayed: true`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotencyConflict"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/support/agent/tickets/incident/link": {
      "post": {
        "tags": [
          "Support desk — agent"
        ],
        "summary": "Link a ticket as affected by an incident",
        "description": "Both are ticket references.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "support:agent",
        "x-alty-module": "support",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value."
            },
            "required": false,
            "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value.",
            "name": "idempotency-key",
            "in": "header"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "incident": {
                    "type": "string"
                  },
                  "affected": {
                    "type": "string"
                  }
                },
                "required": [
                  "incident",
                  "affected"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The affected ticket.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ticket": {
                      "$ref": "#/components/schemas/Ticket"
                    }
                  },
                  "required": [
                    "ticket"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`invalid_idempotency_key` — the `Idempotency-Key` header is not 1–255 visible ASCII characters. Nothing ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvalidIdempotencyKey"
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it). Also: The key is not bound to an active service account; agent actions must be attributable.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "missing_scope"
                          ]
                        },
                        "needs": {
                          "type": "string",
                          "description": "The scope this endpoint needs. Ask your Alty admin for a key that has it."
                        }
                      },
                      "required": [
                        "error",
                        "needs"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "module_off"
                          ]
                        },
                        "module": {
                          "type": "string",
                          "description": "The module that is switched off for this instance, e.g. `support`."
                        }
                      },
                      "required": [
                        "error",
                        "module"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Not found — the same answer whether it does not exist or is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_conflict` — this `Idempotency-Key` was already used with a different request (method, path or body): use a new value for a new request. `idempotency_in_progress` — the first request with this value is still running: wait for `Retry-After` (seconds) and retry with the SAME value. A completed request is never a 409: a retry with the same value and body gets the original status code and body back, with the header `idempotent-replayed: true`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotencyConflict"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/support/agent/tickets/incident/unlink": {
      "post": {
        "tags": [
          "Support desk — agent"
        ],
        "summary": "Detach a ticket from its incident",
        "description": "Takes the affected ticket reference.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "support:agent",
        "x-alty-module": "support",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value."
            },
            "required": false,
            "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value.",
            "name": "idempotency-key",
            "in": "header"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "affected": {
                    "type": "string"
                  }
                },
                "required": [
                  "affected"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The ticket.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ticket": {
                      "$ref": "#/components/schemas/Ticket"
                    }
                  },
                  "required": [
                    "ticket"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`invalid_idempotency_key` — the `Idempotency-Key` header is not 1–255 visible ASCII characters. Nothing ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvalidIdempotencyKey"
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it). Also: The key is not bound to an active service account; agent actions must be attributable.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "missing_scope"
                          ]
                        },
                        "needs": {
                          "type": "string",
                          "description": "The scope this endpoint needs. Ask your Alty admin for a key that has it."
                        }
                      },
                      "required": [
                        "error",
                        "needs"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "module_off"
                          ]
                        },
                        "module": {
                          "type": "string",
                          "description": "The module that is switched off for this instance, e.g. `support`."
                        }
                      },
                      "required": [
                        "error",
                        "module"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Not found — the same answer whether it does not exist or is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_conflict` — this `Idempotency-Key` was already used with a different request (method, path or body): use a new value for a new request. `idempotency_in_progress` — the first request with this value is still running: wait for `Retry-After` (seconds) and retry with the SAME value. A completed request is never a 409: a retry with the same value and body gets the original status code and body back, with the header `idempotent-replayed: true`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotencyConflict"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/support/agent/tickets/incident/affect": {
      "post": {
        "tags": [
          "Support desk — agent"
        ],
        "summary": "Raise an affected ticket for one customer",
        "description": "Under the service account, or the `requester_email` you name. Name the customer by id or by your external ref for it. A service account with no email (as the MCP setup card makes) gets a 400 unless `requester_email` is sent. Never the incident’s requester, who may be another customer.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "support:agent",
        "x-alty-module": "support",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value."
            },
            "required": false,
            "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value.",
            "name": "idempotency-key",
            "in": "header"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "incident": {
                    "type": "string"
                  },
                  "customer_id": {
                    "type": "string"
                  },
                  "customer_external_ref": {
                    "type": "string"
                  },
                  "requester_email": {
                    "type": "string",
                    "description": "Raise it under this address. Omitted: the service account."
                  }
                },
                "required": [
                  "incident"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new ticket.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ticket": {
                      "$ref": "#/components/schemas/Ticket"
                    }
                  },
                  "required": [
                    "ticket"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`invalid_idempotency_key` — the `Idempotency-Key` header is not 1–255 visible ASCII characters. Nothing ran. Also: The request body is invalid; `error` says which field.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/InvalidIdempotencyKey"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it). Also: The key is not bound to an active service account; agent actions must be attributable.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "missing_scope"
                          ]
                        },
                        "needs": {
                          "type": "string",
                          "description": "The scope this endpoint needs. Ask your Alty admin for a key that has it."
                        }
                      },
                      "required": [
                        "error",
                        "needs"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "module_off"
                          ]
                        },
                        "module": {
                          "type": "string",
                          "description": "The module that is switched off for this instance, e.g. `support`."
                        }
                      },
                      "required": [
                        "error",
                        "module"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Not found — the same answer whether it does not exist or is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_conflict` — this `Idempotency-Key` was already used with a different request (method, path or body): use a new value for a new request. `idempotency_in_progress` — the first request with this value is still running: wait for `Retry-After` (seconds) and retry with the SAME value. A completed request is never a 409: a retry with the same value and body gets the original status code and body back, with the header `idempotent-replayed: true`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotencyConflict"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/support/agent/tickets/create": {
      "post": {
        "tags": [
          "Support desk — agent"
        ],
        "summary": "Raise a ticket",
        "description": "As staff raise one from the console. The requester is the service account unless you name one with `requester_email` — and the service account needs an email for that; one without (as the MCP setup card makes) gets a 400 unless `requester_email` is sent. `source_ref` is not confined on this plane. A ticket linked to a customer starts with that customer’s primary contact when the requester is not already one of its contacts.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "support:agent",
        "x-alty-module": "support",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value."
            },
            "required": false,
            "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value.",
            "name": "idempotency-key",
            "in": "header"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "subject": {
                    "type": "string"
                  },
                  "body": {
                    "type": "string"
                  },
                  "type": {
                    "type": "string",
                    "description": "The desk’s ticket type, e.g. BUG, FEATURE, QUESTION."
                  },
                  "priority": {
                    "type": "string"
                  },
                  "requester_email": {
                    "type": "string",
                    "description": "Raise it on this person’s behalf. Omitted: the service account."
                  },
                  "requester_name": {
                    "type": "string"
                  },
                  "source_ref": {
                    "type": "string"
                  },
                  "customer_id": {
                    "type": "string"
                  },
                  "customer_external_ref": {
                    "type": "string",
                    "description": "Link to an EXISTING customer by your own ref for it; unknown leaves the ticket unlinked."
                  }
                },
                "required": [
                  "subject",
                  "body"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new ticket.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ticket": {
                      "$ref": "#/components/schemas/Ticket"
                    }
                  },
                  "required": [
                    "ticket"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`invalid_idempotency_key` — the `Idempotency-Key` header is not 1–255 visible ASCII characters. Nothing ran. Also: The request body is invalid; `error` says which field.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/InvalidIdempotencyKey"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it). Also: The key is not bound to an active service account; agent actions must be attributable.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "missing_scope"
                          ]
                        },
                        "needs": {
                          "type": "string",
                          "description": "The scope this endpoint needs. Ask your Alty admin for a key that has it."
                        }
                      },
                      "required": [
                        "error",
                        "needs"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "module_off"
                          ]
                        },
                        "module": {
                          "type": "string",
                          "description": "The module that is switched off for this instance, e.g. `support`."
                        }
                      },
                      "required": [
                        "error",
                        "module"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_conflict` — this `Idempotency-Key` was already used with a different request (method, path or body): use a new value for a new request. `idempotency_in_progress` — the first request with this value is still running: wait for `Retry-After` (seconds) and retry with the SAME value. A completed request is never a 409: a retry with the same value and body gets the original status code and body back, with the header `idempotent-replayed: true`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotencyConflict"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/support/agent/tickets/note": {
      "post": {
        "tags": [
          "Support desk — agent"
        ],
        "summary": "Add an internal note",
        "description": "Never visible to the requester.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "support:agent",
        "x-alty-module": "support",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value."
            },
            "required": false,
            "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value.",
            "name": "idempotency-key",
            "in": "header"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reference": {
                    "type": "string"
                  },
                  "body": {
                    "type": "string"
                  }
                },
                "required": [
                  "reference",
                  "body"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The ticket.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ticket": {
                      "$ref": "#/components/schemas/Ticket"
                    }
                  },
                  "required": [
                    "ticket"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`invalid_idempotency_key` — the `Idempotency-Key` header is not 1–255 visible ASCII characters. Nothing ran. Also: The request body is invalid; `error` says which field.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/InvalidIdempotencyKey"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it). Also: The key is not bound to an active service account; agent actions must be attributable.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "missing_scope"
                          ]
                        },
                        "needs": {
                          "type": "string",
                          "description": "The scope this endpoint needs. Ask your Alty admin for a key that has it."
                        }
                      },
                      "required": [
                        "error",
                        "needs"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "module_off"
                          ]
                        },
                        "module": {
                          "type": "string",
                          "description": "The module that is switched off for this instance, e.g. `support`."
                        }
                      },
                      "required": [
                        "error",
                        "module"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Not found — the same answer whether it does not exist or is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_conflict` — this `Idempotency-Key` was already used with a different request (method, path or body): use a new value for a new request. `idempotency_in_progress` — the first request with this value is still running: wait for `Retry-After` (seconds) and retry with the SAME value. A completed request is never a 409: a retry with the same value and body gets the original status code and body back, with the header `idempotent-replayed: true`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotencyConflict"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/support/agent/tickets/assign": {
      "post": {
        "tags": [
          "Support desk — agent"
        ],
        "summary": "Assign or unclaim a ticket",
        "description": "`assignee` is a member id, \"me\" (the service account) or null.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "support:agent",
        "x-alty-module": "support",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value."
            },
            "required": false,
            "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value.",
            "name": "idempotency-key",
            "in": "header"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reference": {
                    "type": "string"
                  },
                  "assignee": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                },
                "required": [
                  "reference",
                  "assignee"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The ticket.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ticket": {
                      "$ref": "#/components/schemas/Ticket"
                    }
                  },
                  "required": [
                    "ticket"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`invalid_idempotency_key` — the `Idempotency-Key` header is not 1–255 visible ASCII characters. Nothing ran. Also: The request body is invalid; `error` says which field.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/InvalidIdempotencyKey"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it). Also: The key is not bound to an active service account; agent actions must be attributable.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "missing_scope"
                          ]
                        },
                        "needs": {
                          "type": "string",
                          "description": "The scope this endpoint needs. Ask your Alty admin for a key that has it."
                        }
                      },
                      "required": [
                        "error",
                        "needs"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "module_off"
                          ]
                        },
                        "module": {
                          "type": "string",
                          "description": "The module that is switched off for this instance, e.g. `support`."
                        }
                      },
                      "required": [
                        "error",
                        "module"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Not found — the same answer whether it does not exist or is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_conflict` — this `Idempotency-Key` was already used with a different request (method, path or body): use a new value for a new request. `idempotency_in_progress` — the first request with this value is still running: wait for `Retry-After` (seconds) and retry with the SAME value. A completed request is never a 409: a retry with the same value and body gets the original status code and body back, with the header `idempotent-replayed: true`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotencyConflict"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/support/agent/tickets/link": {
      "post": {
        "tags": [
          "Support desk — agent"
        ],
        "summary": "Link a ticket to a customer by your ref for it",
        "description": "An unknown ref is a 404. A ticket left with no contacts starts with the customer’s primary contact (one with an email), as an affected ticket does.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "support:agent",
        "x-alty-module": "support",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value."
            },
            "required": false,
            "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value.",
            "name": "idempotency-key",
            "in": "header"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reference": {
                    "type": "string"
                  },
                  "customer_external_ref": {
                    "type": "string"
                  }
                },
                "required": [
                  "reference",
                  "customer_external_ref"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The ticket.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ticket": {
                      "$ref": "#/components/schemas/Ticket"
                    }
                  },
                  "required": [
                    "ticket"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`invalid_idempotency_key` — the `Idempotency-Key` header is not 1–255 visible ASCII characters. Nothing ran. Also: The request body is invalid; `error` says which field.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/InvalidIdempotencyKey"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it). Also: The key is not bound to an active service account; agent actions must be attributable.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "missing_scope"
                          ]
                        },
                        "needs": {
                          "type": "string",
                          "description": "The scope this endpoint needs. Ask your Alty admin for a key that has it."
                        }
                      },
                      "required": [
                        "error",
                        "needs"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "module_off"
                          ]
                        },
                        "module": {
                          "type": "string",
                          "description": "The module that is switched off for this instance, e.g. `support`."
                        }
                      },
                      "required": [
                        "error",
                        "module"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Not found — the same answer whether it does not exist or is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_conflict` — this `Idempotency-Key` was already used with a different request (method, path or body): use a new value for a new request. `idempotency_in_progress` — the first request with this value is still running: wait for `Retry-After` (seconds) and retry with the SAME value. A completed request is never a 409: a retry with the same value and body gets the original status code and body back, with the header `idempotent-replayed: true`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotencyConflict"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/support/agent/attachments/get": {
      "post": {
        "tags": [
          "Support desk — agent"
        ],
        "summary": "Download a file from a ticket",
        "description": "Notes included (staff view).",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "support:agent",
        "x-alty-module": "support",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value."
            },
            "required": false,
            "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value.",
            "name": "idempotency-key",
            "in": "header"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reference": {
                    "type": "string"
                  },
                  "attachment_id": {
                    "type": "string"
                  }
                },
                "required": [
                  "reference",
                  "attachment_id"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The file, with download headers and `nosniff`.",
            "content": {
              "application/octet-stream": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_idempotency_key` — the `Idempotency-Key` header is not 1–255 visible ASCII characters. Nothing ran. Also: The request body is invalid; `error` says which field.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/InvalidIdempotencyKey"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it). Also: The key is not bound to an active service account; agent actions must be attributable.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "missing_scope"
                          ]
                        },
                        "needs": {
                          "type": "string",
                          "description": "The scope this endpoint needs. Ask your Alty admin for a key that has it."
                        }
                      },
                      "required": [
                        "error",
                        "needs"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "module_off"
                          ]
                        },
                        "module": {
                          "type": "string",
                          "description": "The module that is switched off for this instance, e.g. `support`."
                        }
                      },
                      "required": [
                        "error",
                        "module"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Not found — the same answer whether it does not exist or is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_conflict` — this `Idempotency-Key` was already used with a different request (method, path or body): use a new value for a new request. `idempotency_in_progress` — the first request with this value is still running: wait for `Retry-After` (seconds) and retry with the SAME value. A completed request is never a 409: a retry with the same value and body gets the original status code and body back, with the header `idempotent-replayed: true`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotencyConflict"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "503": {
            "description": "A dependency (file storage, PDF service) is unavailable; retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/support/agent/attachments/put": {
      "post": {
        "tags": [
          "Support desk — agent"
        ],
        "summary": "Reply or add a note WITH files",
        "description": "The words are written first; each refused file is reported in `attachment_failures`.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "support:agent",
        "x-alty-module": "support",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value."
            },
            "required": false,
            "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value.",
            "name": "idempotency-key",
            "in": "header"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reference": {
                    "type": "string"
                  },
                  "body": {
                    "type": "string"
                  },
                  "internal": {
                    "type": "boolean"
                  },
                  "attachments": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/AttachmentIn"
                    }
                  }
                },
                "required": [
                  "reference",
                  "body",
                  "attachments"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The ticket and the stored files.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ticket": {
                      "$ref": "#/components/schemas/Ticket"
                    },
                    "attachments": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Attachment"
                      }
                    },
                    "attachment_failures": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Each file that did not store, and why. The words were saved regardless."
                    }
                  },
                  "required": [
                    "ticket"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`invalid_idempotency_key` — the `Idempotency-Key` header is not 1–255 visible ASCII characters. Nothing ran. Also: The request body is invalid; `error` says which field.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/InvalidIdempotencyKey"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it). Also: The key is not bound to an active service account; agent actions must be attributable.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "missing_scope"
                          ]
                        },
                        "needs": {
                          "type": "string",
                          "description": "The scope this endpoint needs. Ask your Alty admin for a key that has it."
                        }
                      },
                      "required": [
                        "error",
                        "needs"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "module_off"
                          ]
                        },
                        "module": {
                          "type": "string",
                          "description": "The module that is switched off for this instance, e.g. `support`."
                        }
                      },
                      "required": [
                        "error",
                        "module"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Not found — the same answer whether it does not exist or is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_conflict` — this `Idempotency-Key` was already used with a different request (method, path or body): use a new value for a new request. `idempotency_in_progress` — the first request with this value is still running: wait for `Retry-After` (seconds) and retry with the SAME value. A completed request is never a 409: a retry with the same value and body gets the original status code and body back, with the header `idempotent-replayed: true`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotencyConflict"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/intake/enquiries": {
      "post": {
        "tags": [
          "Website intake"
        ],
        "summary": "Post a website enquiry as a new lead",
        "description": "Server-side only: never put an intake key in browser code. Creates a lead (source `WEBSITE` where the instance has that source) and returns its id and number; the 201 body is the confirmation that it landed. Text fields are trimmed, then checked against the lengths below: an empty required field or an over-long one is 400 with `error` naming it. A `service_type` that is not one of the instance’s service type keys is ignored: the lead is saved without one.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "enquiry:intake",
        "x-alty-module": "core",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value."
            },
            "required": false,
            "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value.",
            "name": "idempotency-key",
            "in": "header"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "subject": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 200,
                    "description": "The lead's title: what it is called in the Leads list and on the lead's page. For a plain contact form, a fixed string such as \"Website enquiry\" or the form's name."
                  },
                  "contact_name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 200
                  },
                  "contact_email": {
                    "type": "string",
                    "minLength": 3,
                    "maxLength": 254,
                    "description": "An email address (something@domain.tld). Anything else is 400."
                  },
                  "message": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 20000,
                    "description": "What the visitor wrote. Kept as the lead's notes."
                  },
                  "contact_phone": {
                    "type": "string",
                    "maxLength": 40
                  },
                  "company_name": {
                    "type": "string",
                    "maxLength": 200
                  },
                  "country": {
                    "type": "string",
                    "maxLength": 2,
                    "description": "ISO 3166 alpha-2, e.g. `AU`. Stored upper-case."
                  },
                  "service_type": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "A key from the instance’s service types picklist. Unknown keys are ignored."
                  },
                  "website": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "The visitor's business website, e.g. `example.com` or `https://www.example.com/`. Kept on the lead as an https address; anything that is not a plain website address is ignored, never a 400. When the lead is converted, a new customer gets it as its website and the logo found there."
                  }
                },
                "required": [
                  "subject",
                  "contact_name",
                  "contact_email",
                  "message"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The lead. `enquiry_number` is an opaque display reference: show or log it, never parse it.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "enquiry_id": {
                      "type": "string",
                      "description": "The lead's id: use it with the other intake endpoints."
                    },
                    "enquiry_number": {
                      "type": "string",
                      "description": "The lead's reference as the business sees it in Leads. Its format is the instance's and may change."
                    }
                  },
                  "required": [
                    "enquiry_id",
                    "enquiry_number"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`invalid_idempotency_key` — the `Idempotency-Key` header is not 1–255 visible ASCII characters. Nothing ran. Also: The request body is invalid; `error` says which field.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/InvalidIdempotencyKey"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it). Also: Leads (enquiries) are not in use on this instance.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "missing_scope"
                          ]
                        },
                        "needs": {
                          "type": "string",
                          "description": "The scope this endpoint needs. Ask your Alty admin for a key that has it."
                        }
                      },
                      "required": [
                        "error",
                        "needs"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "module_off"
                          ]
                        },
                        "module": {
                          "type": "string",
                          "description": "The module that is switched off for this instance, e.g. `support`."
                        }
                      },
                      "required": [
                        "error",
                        "module"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_conflict` — this `Idempotency-Key` was already used with a different request (method, path or body): use a new value for a new request. `idempotency_in_progress` — the first request with this value is still running: wait for `Retry-After` (seconds) and retry with the SAME value. A completed request is never a 409: a retry with the same value and body gets the original status code and body back, with the header `idempotent-replayed: true`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotencyConflict"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs. Also: Over the instance intake limit (60 writes a minute); retry shortly.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/KeyRateLimited"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/intake/discoveries": {
      "post": {
        "tags": [
          "Website intake"
        ],
        "summary": "Post discovery answers for a lead",
        "description": "A discovery questionnaire, shown on the lead. Attaches to an existing lead by `enquiry_id` (404 if there is none; 409 if it already has a discovery), or creates a lead. Same trimming and length rules as a website enquiry.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "enquiry:intake",
        "x-alty-module": "core",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value."
            },
            "required": false,
            "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value.",
            "name": "idempotency-key",
            "in": "header"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "enquiry_id": {
                    "type": "string",
                    "description": "From a 201 above, or from the link the lead was sent."
                  },
                  "business_name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 200
                  },
                  "contact_name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 200
                  },
                  "contact_email": {
                    "type": "string",
                    "minLength": 3,
                    "maxLength": 254,
                    "description": "An email address (something@domain.tld). Anything else is 400."
                  },
                  "contact_phone": {
                    "type": "string",
                    "maxLength": 40
                  },
                  "team_size": {
                    "type": "string",
                    "maxLength": 200
                  },
                  "pain_points": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 200
                    },
                    "maxItems": 50,
                    "description": "At most 50 entries of at most 200 characters."
                  },
                  "tools_in_use": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 200
                    },
                    "maxItems": 50,
                    "description": "At most 50 entries of at most 200 characters."
                  },
                  "frustrated_rating": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 5
                  },
                  "time_wasted_rating": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 5
                  },
                  "error_prone_rating": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 5
                  },
                  "snapshot_ease_rating": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 5
                  },
                  "biggest_frustration": {
                    "type": "string",
                    "maxLength": 5000
                  },
                  "dream_scenario": {
                    "type": "string",
                    "maxLength": 5000
                  },
                  "additional_notes": {
                    "type": "string",
                    "maxLength": 5000
                  }
                },
                "required": [
                  "business_name",
                  "contact_name",
                  "contact_email"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The lead and the discovery reference.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "enquiry_id": {
                      "type": "string"
                    },
                    "enquiry_number": {
                      "type": "string"
                    },
                    "discovery_number": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "enquiry_id",
                    "enquiry_number",
                    "discovery_number"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`invalid_idempotency_key` — the `Idempotency-Key` header is not 1–255 visible ASCII characters. Nothing ran. Also: The request body is invalid; `error` says which field.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/InvalidIdempotencyKey"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it). Also: Leads (enquiries) are not in use on this instance.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "missing_scope"
                          ]
                        },
                        "needs": {
                          "type": "string",
                          "description": "The scope this endpoint needs. Ask your Alty admin for a key that has it."
                        }
                      },
                      "required": [
                        "error",
                        "needs"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "module_off"
                          ]
                        },
                        "module": {
                          "type": "string",
                          "description": "The module that is switched off for this instance, e.g. `support`."
                        }
                      },
                      "required": [
                        "error",
                        "module"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "`enquiry_id` names no lead.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_conflict` — this `Idempotency-Key` was already used with a different request (method, path or body): use a new value for a new request. `idempotency_in_progress` — the first request with this value is still running: wait for `Retry-After` (seconds) and retry with the SAME value. A completed request is never a 409: a retry with the same value and body gets the original status code and body back, with the header `idempotent-replayed: true`. Also: That lead already has a discovery.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/IdempotencyConflict"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs. Also: Over the instance intake limit (60 writes a minute); retry shortly.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/KeyRateLimited"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/intake/discoveries/{id}/summary-pdf": {
      "post": {
        "tags": [
          "Website intake"
        ],
        "summary": "Attach the discovery summary PDF to a lead",
        "description": "At most 2 MB. Replaces any earlier summary.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "enquiry:intake",
        "x-alty-module": "core",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "description": "The enquiry id."
            },
            "required": true,
            "description": "The enquiry id.",
            "name": "id",
            "in": "path"
          },
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value."
            },
            "required": false,
            "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value.",
            "name": "idempotency-key",
            "in": "header"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "summary_pdf_base64": {
                    "type": "string"
                  }
                },
                "required": [
                  "summary_pdf_base64"
                ]
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Stored."
          },
          "400": {
            "description": "`invalid_idempotency_key` — the `Idempotency-Key` header is not 1–255 visible ASCII characters. Nothing ran. Also: The request body is invalid; `error` says which field.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/InvalidIdempotencyKey"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it). Also: Leads (enquiries) are not in use on this instance.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "missing_scope"
                          ]
                        },
                        "needs": {
                          "type": "string",
                          "description": "The scope this endpoint needs. Ask your Alty admin for a key that has it."
                        }
                      },
                      "required": [
                        "error",
                        "needs"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "module_off"
                          ]
                        },
                        "module": {
                          "type": "string",
                          "description": "The module that is switched off for this instance, e.g. `support`."
                        }
                      },
                      "required": [
                        "error",
                        "module"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_conflict` — this `Idempotency-Key` was already used with a different request (method, path or body): use a new value for a new request. `idempotency_in_progress` — the first request with this value is still running: wait for `Retry-After` (seconds) and retry with the SAME value. A completed request is never a 409: a retry with the same value and body gets the original status code and body back, with the header `idempotent-replayed: true`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotencyConflict"
                }
              }
            }
          },
          "413": {
            "description": "The PDF is over 2 MB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs. Also: Over the instance intake limit (60 writes a minute); retry shortly.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/KeyRateLimited"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/intake/enquiries/{id}/prefill": {
      "get": {
        "tags": [
          "Website intake"
        ],
        "summary": "Prefill the discovery form from a lead",
        "description": "For the link a lead was sent; records that the form was opened. 410 once the link is over its age limit.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "enquiry:intake",
        "x-alty-module": "core",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "description": "The enquiry id."
            },
            "required": true,
            "description": "The enquiry id.",
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The contact details.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "contact_name": {
                      "type": "string"
                    },
                    "contact_email": {
                      "type": "string"
                    },
                    "company_name": {
                      "type": "string"
                    },
                    "first_name": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "contact_name",
                    "contact_email",
                    "company_name",
                    "first_name"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it). Also: Leads (enquiries) are not in use on this instance.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "missing_scope"
                          ]
                        },
                        "needs": {
                          "type": "string",
                          "description": "The scope this endpoint needs. Ask your Alty admin for a key that has it."
                        }
                      },
                      "required": [
                        "error",
                        "needs"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "module_off"
                          ]
                        },
                        "module": {
                          "type": "string",
                          "description": "The module that is switched off for this instance, e.g. `support`."
                        }
                      },
                      "required": [
                        "error",
                        "module"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Not found — the same answer whether it does not exist or is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "410": {
            "description": "The link has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/proposals/{token}": {
      "get": {
        "tags": [
          "Hosted proposals"
        ],
        "summary": "Read a proposal by its token",
        "description": "What a site shows the customer before they accept or decline.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "enquiry:intake",
        "x-alty-module": "core",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "description": "The proposal token from the link the customer received."
            },
            "required": true,
            "description": "The proposal token from the link the customer received.",
            "name": "token",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The proposal.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "proposal": {
                      "$ref": "#/components/schemas/Proposal"
                    }
                  },
                  "required": [
                    "proposal"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it). Also: Leads (enquiries) are not in use on this instance.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "missing_scope"
                          ]
                        },
                        "needs": {
                          "type": "string",
                          "description": "The scope this endpoint needs. Ask your Alty admin for a key that has it."
                        }
                      },
                      "required": [
                        "error",
                        "needs"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "module_off"
                          ]
                        },
                        "module": {
                          "type": "string",
                          "description": "The module that is switched off for this instance, e.g. `support`."
                        }
                      },
                      "required": [
                        "error",
                        "module"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Not found — the same answer whether it does not exist or is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The proposal was already accepted or declined.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "410": {
            "description": "The proposal link has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/proposals/{token}/accept": {
      "post": {
        "tags": [
          "Hosted proposals"
        ],
        "summary": "Accept a proposal",
        "description": "Once only; a second answer is 409.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "enquiry:intake",
        "x-alty-module": "core",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "description": "The proposal token from the link the customer received."
            },
            "required": true,
            "description": "The proposal token from the link the customer received.",
            "name": "token",
            "in": "path"
          },
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value."
            },
            "required": false,
            "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value.",
            "name": "idempotency-key",
            "in": "header"
          }
        ],
        "responses": {
          "200": {
            "description": "Accepted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  },
                  "required": [
                    "ok"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`invalid_idempotency_key` — the `Idempotency-Key` header is not 1–255 visible ASCII characters. Nothing ran.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvalidIdempotencyKey"
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it). Also: Leads (enquiries) are not in use on this instance.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "missing_scope"
                          ]
                        },
                        "needs": {
                          "type": "string",
                          "description": "The scope this endpoint needs. Ask your Alty admin for a key that has it."
                        }
                      },
                      "required": [
                        "error",
                        "needs"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "module_off"
                          ]
                        },
                        "module": {
                          "type": "string",
                          "description": "The module that is switched off for this instance, e.g. `support`."
                        }
                      },
                      "required": [
                        "error",
                        "module"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Not found — the same answer whether it does not exist or is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_conflict` — this `Idempotency-Key` was already used with a different request (method, path or body): use a new value for a new request. `idempotency_in_progress` — the first request with this value is still running: wait for `Retry-After` (seconds) and retry with the SAME value. A completed request is never a 409: a retry with the same value and body gets the original status code and body back, with the header `idempotent-replayed: true`. Also: The proposal was already accepted or declined.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/IdempotencyConflict"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "410": {
            "description": "The proposal link has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs. Also: Over the instance intake limit (60 writes a minute); retry shortly.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/KeyRateLimited"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/proposals/{token}/decline": {
      "post": {
        "tags": [
          "Hosted proposals"
        ],
        "summary": "Decline a proposal",
        "description": "Once only; a second answer is 409.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "enquiry:intake",
        "x-alty-module": "core",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "description": "The proposal token from the link the customer received."
            },
            "required": true,
            "description": "The proposal token from the link the customer received.",
            "name": "token",
            "in": "path"
          },
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value."
            },
            "required": false,
            "description": "Optional, 1–255 visible ASCII characters. Send the same value to retry the SAME request safely for 24 hours: you get the first response back (header `idempotent-replayed: true`) and nothing happens twice. The same value with a different body is 409 `idempotency_conflict`; while the first is still running, 409 `idempotency_in_progress` with `Retry-After`. 5xx and 429 answers are not kept, so retrying those runs again. After fixing a refused request, use a new value.",
            "name": "idempotency-key",
            "in": "header"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string",
                    "enum": [
                      "PRICE",
                      "FOUND_ANOTHER",
                      "TIMING",
                      "DIFFERENT_DIRECTION",
                      "OTHER"
                    ],
                    "description": "Required; anything else is 400."
                  },
                  "detail": {
                    "type": "string",
                    "maxLength": 500,
                    "description": "What the customer added, in their words."
                  }
                },
                "required": [
                  "reason"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Declined.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  },
                  "required": [
                    "ok"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`invalid_idempotency_key` — the `Idempotency-Key` header is not 1–255 visible ASCII characters. Nothing ran. Also: The request body is invalid; `error` says which field.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/InvalidIdempotencyKey"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it). Also: Leads (enquiries) are not in use on this instance.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "missing_scope"
                          ]
                        },
                        "needs": {
                          "type": "string",
                          "description": "The scope this endpoint needs. Ask your Alty admin for a key that has it."
                        }
                      },
                      "required": [
                        "error",
                        "needs"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "module_off"
                          ]
                        },
                        "module": {
                          "type": "string",
                          "description": "The module that is switched off for this instance, e.g. `support`."
                        }
                      },
                      "required": [
                        "error",
                        "module"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Not found — the same answer whether it does not exist or is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_conflict` — this `Idempotency-Key` was already used with a different request (method, path or body): use a new value for a new request. `idempotency_in_progress` — the first request with this value is still running: wait for `Retry-After` (seconds) and retry with the SAME value. A completed request is never a 409: a retry with the same value and body gets the original status code and body back, with the header `idempotent-replayed: true`. Also: The proposal was already accepted or declined.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/IdempotencyConflict"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "410": {
            "description": "The proposal link has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs. Also: Over the instance intake limit (60 writes a minute); retry shortly.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/KeyRateLimited"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    },
    "/api/public/v1/proposals/{token}/pdf": {
      "get": {
        "tags": [
          "Hosted proposals"
        ],
        "summary": "Download the proposal PDF",
        "description": "Rendered on request.",
        "security": [
          {
            "altyKey": []
          }
        ],
        "x-alty-scope": "enquiry:intake",
        "x-alty-module": "core",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "description": "The proposal token from the link the customer received."
            },
            "required": true,
            "description": "The proposal token from the link the customer received.",
            "name": "token",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The PDF.",
            "content": {
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "401": {
            "description": "The uniform refusal: no key, or a wrong, revoked or expired one, or API access is off for this instance. The same answer in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Unauthorized"
                }
              }
            }
          },
          "403": {
            "description": "Your key is valid, but: `missing_scope` — it lacks the scope this endpoint needs (`needs` names it); or `module_off` — the module (or its API channel) is switched off for this instance (`module` names it). Also: Leads (enquiries) are not in use on this instance.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "missing_scope"
                          ]
                        },
                        "needs": {
                          "type": "string",
                          "description": "The scope this endpoint needs. Ask your Alty admin for a key that has it."
                        }
                      },
                      "required": [
                        "error",
                        "needs"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string",
                          "enum": [
                            "module_off"
                          ]
                        },
                        "module": {
                          "type": "string",
                          "description": "The module that is switched off for this instance, e.g. `support`."
                        }
                      },
                      "required": [
                        "error",
                        "module"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Not found — the same answer whether it does not exist or is not yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The proposal was already accepted or declined.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "410": {
            "description": "The proposal link has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Your key is over its rate limit: about 120 requests a minute. Wait for the `Retry-After` header (seconds) and retry. A 429 is never kept for an Idempotency-Key, so the retry runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyRateLimited"
                }
              }
            }
          },
          "503": {
            "description": "A dependency (file storage, PDF service) is unavailable; retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "5XX": {
            "description": "Alty, or a service it depends on, had a problem. Retry with backoff; on a POST send the SAME `Idempotency-Key`: a 5xx is never kept, so the retry runs again rather than replaying the failure."
          }
        }
      }
    }
  },
  "webhooks": {}
}
