{
  "openapi" : "3.1.0",
  "info" : {
    "title" : "CoreSync API",
    "description" : "The public API of CoreSync, Avitech's ISP billing platform: an ISP's subscribers, plans, balances, invoices, payments and live RADIUS sessions, and the writes its staff make every day (record a payment, suspend, reactivate, change plan).\n\n**Authentication.** Send an API key in `X-API-Key`. An ISP creates keys in the CoreSync admin under Settings → API access; a key belongs to that ISP and every call acts on it. Avitech platform keys (`cs_plat_…`) belong to no ISP and name one per call in `X-Tenant-Code`.\n\n**Scopes and permissions.** `read` for every GET; `subscribers:write`, `payments:write` and `webhooks:write` for the writes on an ISP's data; `write` grants all three. A key without the scope gets 403 `insufficient_scope`. A key also never does more than the person who created it: each operation names the CoreSync permission behind it (`x-required-permission`), checked against its creator's current role on every call (403 `key_owner_lacks_permission`).\n\n**Platform keys** (Avitech staff) get more: `listTenants`, `getTenant`, `createTenant` and `sendTenantWelcomeEmail` (scope `tenants:write`), and customer migrations from an old billing system (scope `migrations:write`). These are marked `x-key-kind: PLATFORM`; an ISP's own key gets 403 `platform_key_required`. Platform scopes are never implied by `write`.\n\n**Conventions.** JSON fields are snake_case. Lists take `page` (from 1) and `size` (1–200, default 50) and answer `{items, total, page, size}`. Times are ISO 8601 in UTC; dates are `YYYY-MM-DD`. Money is `{amount: \"1500.00\", currency: \"KES\"}`. Phone numbers are E.164 (`+254712345678`) in and out; any common format is accepted as input.\n\n**Errors** are `application/problem+json` with a stable `code`. **Rate limit:** per key (120 a minute unless set otherwise); every response has `X-RateLimit-Limit` and `X-RateLimit-Remaining`, and a 429 has `Retry-After`.\n\n**Writes.** GET never changes anything. A POST that creates a record or moves money needs an `Idempotency-Key`; repeating it within 24 hours returns the first response (with `Idempotent-Replayed: true`).\n\n**Webhooks** are listed under `webhooks`. Each delivery is signed: `X-Webhook-Signature: t=<unix>,v1=<hex HMAC-SHA256 of \"<t>.<raw body>\">` with the endpoint's secret; the timestamp is also in `X-Webhook-Timestamp`.\n\n**Versioning.** v1 only gains things (new operations, new optional fields) and `info.version` goes up on every change. A breaking change will be published as /api/v2 beside v1, with v1's retirement date announced.",
    "contact" : {
      "name" : "Avitech Solutions",
      "email" : "hello@avitechsolutionsltd.com"
    },
    "license" : {
      "name" : "Proprietary — for use by Avitech's customers and their integrations"
    },
    "version" : "1.0.1"
  },
  "servers" : [ {
    "url" : "https://coresyncapi.avitechcloud.com",
    "description" : "Production"
  } ],
  "security" : [ {
    "ApiKeyAuth" : [ ]
  } ],
  "tags" : [ {
    "name" : "Account",
    "description" : "Who the API key is and which ISP it acts on."
  }, {
    "name" : "Subscribers",
    "description" : "An ISP's customers: who they are, plan, balance, invoices, payments, connection; suspend, reactivate, change plan."
  }, {
    "name" : "Sessions",
    "description" : "Who is online right now."
  }, {
    "name" : "Plans",
    "description" : "The service plans an ISP sells."
  }, {
    "name" : "Invoices",
    "description" : "Invoices issued to subscribers."
  }, {
    "name" : "Payments",
    "description" : "Money received, and recording a payment."
  }, {
    "name" : "Webhooks",
    "description" : "Endpoints that receive signed events."
  }, {
    "name" : "Platform",
    "description" : "Avitech platform keys only: creating ISPs."
  }, {
    "name" : "Migrations",
    "description" : "Avitech platform keys only: moving an ISP's customers from their old billing system."
  } ],
  "paths" : {
    "/api/v1/webhooks" : {
      "get" : {
        "tags" : [ "Webhooks" ],
        "summary" : "List webhook endpoints",
        "description" : "Lists the ISP's webhook endpoints and the events each receives. Signing secrets are not included. Changes nothing.\n\nScope: `read`. The key's creator must hold SYSTEM:SETTINGS:VIEW.",
        "operationId" : "listWebhookEndpoints",
        "parameters" : [ {
          "$ref" : "#/components/parameters/TenantCode"
        } ],
        "responses" : {
          "200" : {
            "description" : "Success.",
            "headers" : {
              "X-RateLimit-Limit" : {
                "$ref" : "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining" : {
                "$ref" : "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content" : {
              "*/*" : {
                "schema" : {
                  "type" : "array",
                  "items" : {
                    "$ref" : "#/components/schemas/WebhookEndpoint"
                  }
                }
              }
            }
          },
          "400" : {
            "description" : "The request is malformed or a parameter is invalid (codes: invalid_request, validation_failed, malformed_body, missing_parameter, invalid_parameter, tenant_required).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "401" : {
            "description" : "No API key, or the key is invalid, revoked or expired (codes: missing_api_key, invalid_api_key, api_key_revoked, api_key_expired).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "403" : {
            "description" : "The key lacks the scope (insufficient_scope), or the ISP's account is suspended or terminated.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "429" : {
            "description" : "Rate limit reached for this key (rate_limited). Wait Retry-After seconds.",
            "headers" : {
              "Retry-After" : {
                "$ref" : "#/components/headers/Retry-After"
              }
            },
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "500" : {
            "description" : "Unexpected error on our side (internal_error).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope" : "read",
        "x-required-permission" : [ "SYSTEM:SETTINGS:VIEW" ],
        "x-changes-data" : false
      },
      "post" : {
        "tags" : [ "Webhooks" ],
        "summary" : "Add a webhook endpoint",
        "description" : "Adds an HTTPS URL that will receive the chosen events. The response carries the signing secret, the only time it is shown: store it to verify X-Webhook-Signature. Creates a record, so it needs an Idempotency-Key. An ISP can have up to 10 endpoints.\n\nScope: `webhooks:write`. The key's creator must hold SYSTEM:SETTINGS:EDIT.",
        "operationId" : "createWebhookEndpoint",
        "parameters" : [ {
          "name" : "Idempotency-Key",
          "in" : "header",
          "description" : "A unique string per new endpoint. Retrying with the same key returns the first response.",
          "required" : true,
          "schema" : {
            "type" : "string"
          },
          "example" : "6f1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d"
        }, {
          "$ref" : "#/components/parameters/TenantCode"
        } ],
        "requestBody" : {
          "content" : {
            "application/json" : {
              "schema" : {
                "$ref" : "#/components/schemas/CreateWebhookEndpointRequest"
              }
            }
          },
          "required" : true
        },
        "responses" : {
          "201" : {
            "description" : "Added. signing_secret is set in this response only.",
            "headers" : {
              "X-RateLimit-Limit" : {
                "$ref" : "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining" : {
                "$ref" : "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content" : {
              "*/*" : {
                "schema" : {
                  "$ref" : "#/components/schemas/WebhookEndpoint"
                }
              }
            }
          },
          "400" : {
            "description" : "The request is malformed or a parameter is invalid (codes: invalid_request, validation_failed, malformed_body, missing_parameter, invalid_parameter, idempotency_key_required, tenant_required).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "401" : {
            "description" : "No API key, or the key is invalid, revoked or expired (codes: missing_api_key, invalid_api_key, api_key_revoked, api_key_expired).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "403" : {
            "description" : "The key lacks the scope (insufficient_scope), or the ISP's account is suspended or terminated.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "409" : {
            "description" : "Conflicts with the current state, e.g. idempotency_in_progress or duplicate_reference.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "422" : {
            "description" : "Refused by a business rule (e.g. invalid_state, same_plan, idempotency_key_reused); detail says why. Nothing was changed.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "429" : {
            "description" : "Rate limit reached for this key (rate_limited). Wait Retry-After seconds.",
            "headers" : {
              "Retry-After" : {
                "$ref" : "#/components/headers/Retry-After"
              }
            },
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "500" : {
            "description" : "Unexpected error on our side (internal_error).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope" : "webhooks:write",
        "x-required-permission" : [ "SYSTEM:SETTINGS:EDIT" ],
        "x-changes-data" : true
      }
    },
    "/api/v1/webhooks/{id}/test" : {
      "post" : {
        "tags" : [ "Webhooks" ],
        "summary" : "Send a test event to an endpoint",
        "description" : "Sends one signed webhook.test event to the endpoint now and returns the delivery, with the HTTP status the endpoint answered. Use it to check a receiver and its signature check. A test is attempted once and never retried; it works on a switched-off endpoint too. Adds one row to the delivery log.\n\nScope: `webhooks:write`. The key's creator must hold SYSTEM:SETTINGS:EDIT.",
        "operationId" : "sendTestWebhookEvent",
        "parameters" : [ {
          "name" : "id",
          "in" : "path",
          "description" : "Endpoint id.",
          "required" : true,
          "schema" : {
            "type" : "string",
            "format" : "uuid"
          },
          "example" : "2b3c4d5e-6f7a-8b9c-0d1e-2f3a4b5c6d7e"
        }, {
          "$ref" : "#/components/parameters/TenantCode"
        } ],
        "responses" : {
          "200" : {
            "description" : "Success.",
            "headers" : {
              "X-RateLimit-Limit" : {
                "$ref" : "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining" : {
                "$ref" : "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content" : {
              "*/*" : {
                "schema" : {
                  "$ref" : "#/components/schemas/WebhookDelivery"
                }
              }
            }
          },
          "400" : {
            "description" : "The request is malformed or a parameter is invalid (codes: invalid_request, validation_failed, malformed_body, missing_parameter, invalid_parameter, idempotency_key_required, tenant_required).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "401" : {
            "description" : "No API key, or the key is invalid, revoked or expired (codes: missing_api_key, invalid_api_key, api_key_revoked, api_key_expired).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "403" : {
            "description" : "The key lacks the scope (insufficient_scope), or the ISP's account is suspended or terminated.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "404" : {
            "description" : "No such record in this ISP's account (not_found). Records of other ISPs also answer 404.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "409" : {
            "description" : "Conflicts with the current state, e.g. idempotency_in_progress or duplicate_reference.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "422" : {
            "description" : "Refused by a business rule (e.g. invalid_state, same_plan, idempotency_key_reused); detail says why. Nothing was changed.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "429" : {
            "description" : "Rate limit reached for this key (rate_limited). Wait Retry-After seconds.",
            "headers" : {
              "Retry-After" : {
                "$ref" : "#/components/headers/Retry-After"
              }
            },
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "500" : {
            "description" : "Unexpected error on our side (internal_error).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope" : "webhooks:write",
        "x-required-permission" : [ "SYSTEM:SETTINGS:EDIT" ],
        "x-changes-data" : true
      }
    },
    "/api/v1/tenants" : {
      "get" : {
        "tags" : [ "Account" ],
        "summary" : "List ISPs (platform keys only)",
        "description" : "Lists every ISP on CoreSync with its code. Only an Avitech platform key may call it; a platform key then passes the chosen code in X-Tenant-Code on every other call. An ISP's own key gets 403. Changes nothing.\n\nPlatform keys only. Scope: `read`. The key's creator must hold SYSTEM:TENANTS:READ or SYSTEM:PLATFORM:MANAGE.",
        "operationId" : "listTenants",
        "parameters" : [ {
          "name" : "status",
          "in" : "query",
          "description" : "Only ISPs with this status.",
          "required" : false,
          "schema" : {
            "type" : "string"
          },
          "example" : "ACTIVE"
        }, {
          "name" : "page",
          "in" : "query",
          "description" : "Page number, starting at 1.",
          "required" : false,
          "schema" : {
            "minimum" : 1,
            "type" : "integer",
            "format" : "int32",
            "default" : 1
          },
          "example" : 1
        }, {
          "name" : "size",
          "in" : "query",
          "description" : "Results per page, 1–200.",
          "required" : false,
          "schema" : {
            "maximum" : 200,
            "minimum" : 1,
            "type" : "integer",
            "format" : "int32",
            "default" : 50
          },
          "example" : 50
        }, {
          "$ref" : "#/components/parameters/TenantCode"
        } ],
        "responses" : {
          "200" : {
            "description" : "Success.",
            "headers" : {
              "X-RateLimit-Limit" : {
                "$ref" : "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining" : {
                "$ref" : "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content" : {
              "*/*" : {
                "schema" : {
                  "$ref" : "#/components/schemas/PagedResultTenant"
                }
              }
            }
          },
          "400" : {
            "description" : "The request is malformed or a parameter is invalid (codes: invalid_request, validation_failed, malformed_body, missing_parameter, invalid_parameter, tenant_required).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "401" : {
            "description" : "No API key, or the key is invalid, revoked or expired (codes: missing_api_key, invalid_api_key, api_key_revoked, api_key_expired).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "403" : {
            "description" : "The key lacks the scope (insufficient_scope), or the ISP's account is suspended or terminated.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "429" : {
            "description" : "Rate limit reached for this key (rate_limited). Wait Retry-After seconds.",
            "headers" : {
              "Retry-After" : {
                "$ref" : "#/components/headers/Retry-After"
              }
            },
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "500" : {
            "description" : "Unexpected error on our side (internal_error).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-key-kind" : "PLATFORM",
        "x-required-scope" : "read",
        "x-required-permission" : [ "SYSTEM:TENANTS:READ", "SYSTEM:PLATFORM:MANAGE" ],
        "x-changes-data" : false
      },
      "post" : {
        "tags" : [ "Platform" ],
        "summary" : "Create a new ISP account (platform keys only)",
        "description" : "Provisions a new ISP on CoreSync exactly as the platform console does: the account, its default roles and settings, the first admin user, its portal address (DNS and certificate) and its VPN endpoints. The admin's password is generated and sent only in the welcome email (now, or later with sendTenantWelcomeEmail when defer_welcome_email is true); it is never returned. Creates a record, so it needs an Idempotency-Key.\n\nPlatform keys only. Scope: `tenants:write`. The key's creator must hold SYSTEM:TENANTS:CREATE or SYSTEM:PLATFORM:MANAGE.",
        "operationId" : "createTenant",
        "parameters" : [ {
          "name" : "Idempotency-Key",
          "in" : "header",
          "description" : "A unique string per new ISP. Retrying with the same key returns the first response.",
          "required" : true,
          "schema" : {
            "type" : "string"
          },
          "example" : "0d2f4c9a-1b3e-4f5a-8c7d-6e5f4a3b2c1d"
        }, {
          "$ref" : "#/components/parameters/TenantCode"
        } ],
        "requestBody" : {
          "content" : {
            "application/json" : {
              "schema" : {
                "$ref" : "#/components/schemas/CreateTenantRequest"
              }
            }
          },
          "required" : true
        },
        "responses" : {
          "201" : {
            "description" : "Provisioned.",
            "headers" : {
              "X-RateLimit-Limit" : {
                "$ref" : "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining" : {
                "$ref" : "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content" : {
              "*/*" : {
                "schema" : {
                  "$ref" : "#/components/schemas/TenantCreated"
                }
              }
            }
          },
          "400" : {
            "description" : "The request is malformed or a parameter is invalid (codes: invalid_request, validation_failed, malformed_body, missing_parameter, invalid_parameter, idempotency_key_required, tenant_required).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "401" : {
            "description" : "No API key, or the key is invalid, revoked or expired (codes: missing_api_key, invalid_api_key, api_key_revoked, api_key_expired).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "403" : {
            "description" : "The key lacks the scope (insufficient_scope), or the ISP's account is suspended or terminated.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "409" : {
            "description" : "Conflicts with the current state, e.g. idempotency_in_progress or duplicate_reference.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "422" : {
            "description" : "Refused by a business rule (e.g. invalid_state, same_plan, idempotency_key_reused); detail says why. Nothing was changed.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "429" : {
            "description" : "Rate limit reached for this key (rate_limited). Wait Retry-After seconds.",
            "headers" : {
              "Retry-After" : {
                "$ref" : "#/components/headers/Retry-After"
              }
            },
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "500" : {
            "description" : "Unexpected error on our side (internal_error).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-key-kind" : "PLATFORM",
        "x-required-scope" : "tenants:write",
        "x-required-permission" : [ "SYSTEM:TENANTS:CREATE", "SYSTEM:PLATFORM:MANAGE" ],
        "x-changes-data" : true
      }
    },
    "/api/v1/tenants/{code}/welcome-email" : {
      "post" : {
        "tags" : [ "Platform" ],
        "summary" : "Send an ISP's admin their login (platform keys only)",
        "description" : "Issues the ISP's first admin a fresh password and emails it with the portal address, as the console's \"Send welcome email\" does. Their previous password stops working. Use it for an ISP created with defer_welcome_email, or when the admin lost the first email.\n\nPlatform keys only. Scope: `tenants:write`. The key's creator must hold SYSTEM:TENANTS:UPDATE or SYSTEM:PLATFORM:MANAGE.",
        "operationId" : "sendTenantWelcomeEmail",
        "parameters" : [ {
          "name" : "code",
          "in" : "path",
          "description" : "The ISP's code.",
          "required" : true,
          "schema" : {
            "type" : "string"
          },
          "example" : "SKYNET"
        }, {
          "$ref" : "#/components/parameters/TenantCode"
        } ],
        "responses" : {
          "200" : {
            "description" : "Success.",
            "headers" : {
              "X-RateLimit-Limit" : {
                "$ref" : "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining" : {
                "$ref" : "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content" : {
              "*/*" : {
                "schema" : {
                  "$ref" : "#/components/schemas/WelcomeEmailResult"
                }
              }
            }
          },
          "400" : {
            "description" : "The request is malformed or a parameter is invalid (codes: invalid_request, validation_failed, malformed_body, missing_parameter, invalid_parameter, idempotency_key_required, tenant_required).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "401" : {
            "description" : "No API key, or the key is invalid, revoked or expired (codes: missing_api_key, invalid_api_key, api_key_revoked, api_key_expired).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "403" : {
            "description" : "The key lacks the scope (insufficient_scope), or the ISP's account is suspended or terminated.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "404" : {
            "description" : "No such record in this ISP's account (not_found). Records of other ISPs also answer 404.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "409" : {
            "description" : "Conflicts with the current state, e.g. idempotency_in_progress or duplicate_reference.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "422" : {
            "description" : "Refused by a business rule (e.g. invalid_state, same_plan, idempotency_key_reused); detail says why. Nothing was changed.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "429" : {
            "description" : "Rate limit reached for this key (rate_limited). Wait Retry-After seconds.",
            "headers" : {
              "Retry-After" : {
                "$ref" : "#/components/headers/Retry-After"
              }
            },
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "500" : {
            "description" : "Unexpected error on our side (internal_error).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-key-kind" : "PLATFORM",
        "x-required-scope" : "tenants:write",
        "x-required-permission" : [ "SYSTEM:TENANTS:UPDATE", "SYSTEM:PLATFORM:MANAGE" ],
        "x-changes-data" : true
      }
    },
    "/api/v1/subscribers/{id}/suspend" : {
      "post" : {
        "tags" : [ "Subscribers" ],
        "summary" : "Suspend a subscriber's service",
        "description" : "Turns the subscriber's internet off now: their status becomes SUSPENDED, RADIUS throttles them (or the router queue is closed for a static line) and any live session is disconnected. Billing records are kept and the subscription end date does not move. If the ISP requires a manager's approval for suspensions, nothing changes yet: an approval request is filed and the answer is 202 with outcome approval_pending. Suspending an already suspended subscriber changes nothing and answers done.\n\nScope: `subscribers:write`. The key's creator must hold BILLING:SUBSCRIBERS:EDIT.",
        "operationId" : "suspendSubscriber",
        "parameters" : [ {
          "name" : "id",
          "in" : "path",
          "description" : "Subscriber id.",
          "required" : true,
          "schema" : {
            "type" : "string",
            "format" : "uuid"
          },
          "example" : "3f2a6c1e-8b7d-4c1a-9e2f-1a2b3c4d5e6f"
        }, {
          "$ref" : "#/components/parameters/TenantCode"
        } ],
        "requestBody" : {
          "content" : {
            "application/json" : {
              "schema" : {
                "$ref" : "#/components/schemas/SuspendSubscriberRequest"
              }
            }
          },
          "required" : true
        },
        "responses" : {
          "200" : {
            "description" : "Suspended (or already suspended).",
            "headers" : {
              "X-RateLimit-Limit" : {
                "$ref" : "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining" : {
                "$ref" : "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content" : {
              "*/*" : {
                "schema" : {
                  "$ref" : "#/components/schemas/SubscriberChange"
                }
              }
            }
          },
          "202" : {
            "description" : "The ISP requires approval; a request was filed.",
            "headers" : {
              "X-RateLimit-Limit" : {
                "$ref" : "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining" : {
                "$ref" : "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content" : {
              "*/*" : {
                "schema" : {
                  "$ref" : "#/components/schemas/SubscriberChange"
                }
              }
            }
          },
          "400" : {
            "description" : "The request is malformed or a parameter is invalid (codes: invalid_request, validation_failed, malformed_body, missing_parameter, invalid_parameter, idempotency_key_required, tenant_required).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "401" : {
            "description" : "No API key, or the key is invalid, revoked or expired (codes: missing_api_key, invalid_api_key, api_key_revoked, api_key_expired).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "403" : {
            "description" : "The key lacks the scope (insufficient_scope), or the ISP's account is suspended or terminated.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "404" : {
            "description" : "No such record in this ISP's account (not_found). Records of other ISPs also answer 404.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "409" : {
            "description" : "Conflicts with the current state, e.g. idempotency_in_progress or duplicate_reference.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "422" : {
            "description" : "Refused by a business rule (e.g. invalid_state, same_plan, idempotency_key_reused); detail says why. Nothing was changed.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "429" : {
            "description" : "Rate limit reached for this key (rate_limited). Wait Retry-After seconds.",
            "headers" : {
              "Retry-After" : {
                "$ref" : "#/components/headers/Retry-After"
              }
            },
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "500" : {
            "description" : "Unexpected error on our side (internal_error).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope" : "subscribers:write",
        "x-required-permission" : [ "BILLING:SUBSCRIBERS:EDIT" ],
        "x-changes-data" : true
      }
    },
    "/api/v1/subscribers/{id}/reactivate" : {
      "post" : {
        "tags" : [ "Subscribers" ],
        "summary" : "Reactivate a suspended subscriber without a payment",
        "description" : "Turns a SUSPENDED subscriber's internet back on without taking money, with a reason recorded on their timeline. If their subscription had already expired it is extended by one plan cycle from now, free of charge, exactly as the portal does. When the customer has paid, use recordPayment instead: it renews and reactivates them properly. Reactivating an ACTIVE subscriber changes nothing and answers done.\n\nScope: `subscribers:write`. The key's creator must hold BILLING:SUBSCRIBERS:EDIT.",
        "operationId" : "reactivateSubscriber",
        "parameters" : [ {
          "name" : "id",
          "in" : "path",
          "description" : "Subscriber id.",
          "required" : true,
          "schema" : {
            "type" : "string",
            "format" : "uuid"
          },
          "example" : "3f2a6c1e-8b7d-4c1a-9e2f-1a2b3c4d5e6f"
        }, {
          "$ref" : "#/components/parameters/TenantCode"
        } ],
        "requestBody" : {
          "content" : {
            "application/json" : {
              "schema" : {
                "$ref" : "#/components/schemas/ReactivateSubscriberRequest"
              }
            }
          },
          "required" : true
        },
        "responses" : {
          "200" : {
            "description" : "Reactivated (or already active).",
            "headers" : {
              "X-RateLimit-Limit" : {
                "$ref" : "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining" : {
                "$ref" : "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content" : {
              "*/*" : {
                "schema" : {
                  "$ref" : "#/components/schemas/SubscriberChange"
                }
              }
            }
          },
          "400" : {
            "description" : "The request is malformed or a parameter is invalid (codes: invalid_request, validation_failed, malformed_body, missing_parameter, invalid_parameter, idempotency_key_required, tenant_required).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "401" : {
            "description" : "No API key, or the key is invalid, revoked or expired (codes: missing_api_key, invalid_api_key, api_key_revoked, api_key_expired).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "403" : {
            "description" : "The key lacks the scope (insufficient_scope), or the ISP's account is suspended or terminated.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "404" : {
            "description" : "No such record in this ISP's account (not_found). Records of other ISPs also answer 404.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "409" : {
            "description" : "Conflicts with the current state, e.g. idempotency_in_progress or duplicate_reference.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "422" : {
            "description" : "Refused by a business rule (e.g. invalid_state, same_plan, idempotency_key_reused); detail says why. Nothing was changed.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "429" : {
            "description" : "Rate limit reached for this key (rate_limited). Wait Retry-After seconds.",
            "headers" : {
              "Retry-After" : {
                "$ref" : "#/components/headers/Retry-After"
              }
            },
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "500" : {
            "description" : "Unexpected error on our side (internal_error).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope" : "subscribers:write",
        "x-required-permission" : [ "BILLING:SUBSCRIBERS:EDIT" ],
        "x-changes-data" : true
      }
    },
    "/api/v1/subscribers/{id}/plan" : {
      "post" : {
        "tags" : [ "Subscribers" ],
        "summary" : "Change a subscriber's plan",
        "description" : "Moves the subscriber to another plan now, without a payment: their speed and price change immediately and the subscription end date stays where it is. The reason is recorded for the ISP's reports. Use listPlans to find plan ids. For a paid upgrade, record the payment as well.\n\nScope: `subscribers:write`. The key's creator must hold BILLING:SUBSCRIPTIONS:CREATE.",
        "operationId" : "changeSubscriberPlan",
        "parameters" : [ {
          "name" : "id",
          "in" : "path",
          "description" : "Subscriber id.",
          "required" : true,
          "schema" : {
            "type" : "string",
            "format" : "uuid"
          },
          "example" : "3f2a6c1e-8b7d-4c1a-9e2f-1a2b3c4d5e6f"
        }, {
          "$ref" : "#/components/parameters/TenantCode"
        } ],
        "requestBody" : {
          "content" : {
            "application/json" : {
              "schema" : {
                "$ref" : "#/components/schemas/ChangePlanRequest"
              }
            }
          },
          "required" : true
        },
        "responses" : {
          "200" : {
            "description" : "Plan changed.",
            "headers" : {
              "X-RateLimit-Limit" : {
                "$ref" : "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining" : {
                "$ref" : "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content" : {
              "*/*" : {
                "schema" : {
                  "$ref" : "#/components/schemas/SubscriberChange"
                }
              }
            }
          },
          "400" : {
            "description" : "The request is malformed or a parameter is invalid (codes: invalid_request, validation_failed, malformed_body, missing_parameter, invalid_parameter, idempotency_key_required, tenant_required).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "401" : {
            "description" : "No API key, or the key is invalid, revoked or expired (codes: missing_api_key, invalid_api_key, api_key_revoked, api_key_expired).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "403" : {
            "description" : "The key lacks the scope (insufficient_scope), or the ISP's account is suspended or terminated.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "404" : {
            "description" : "No such record in this ISP's account (not_found). Records of other ISPs also answer 404.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "409" : {
            "description" : "Conflicts with the current state, e.g. idempotency_in_progress or duplicate_reference.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "422" : {
            "description" : "Refused by a business rule (e.g. invalid_state, same_plan, idempotency_key_reused); detail says why. Nothing was changed.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "429" : {
            "description" : "Rate limit reached for this key (rate_limited). Wait Retry-After seconds.",
            "headers" : {
              "Retry-After" : {
                "$ref" : "#/components/headers/Retry-After"
              }
            },
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "500" : {
            "description" : "Unexpected error on our side (internal_error).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope" : "subscribers:write",
        "x-required-permission" : [ "BILLING:SUBSCRIPTIONS:CREATE" ],
        "x-changes-data" : true
      }
    },
    "/api/v1/payments" : {
      "get" : {
        "tags" : [ "Payments" ],
        "summary" : "List payments",
        "description" : "Lists payments received, newest first, by status, provider, whether they matched a subscriber, and time received. matched=false lists money that arrived but could not be tied to a subscriber. Changes nothing.\n\nScope: `read`. The key's creator must hold BILLING:PAYMENTS:VIEW.",
        "operationId" : "listPayments",
        "parameters" : [ {
          "name" : "status",
          "in" : "query",
          "description" : "Only payments in this state.",
          "required" : false,
          "schema" : {
            "type" : "string",
            "enum" : [ "PENDING", "COMPLETED", "FAILED", "REVERSED" ]
          }
        }, {
          "name" : "provider",
          "in" : "query",
          "description" : "Only this provider, e.g. MPESA, KCB, CASH.",
          "required" : false,
          "schema" : {
            "type" : "string"
          },
          "example" : "MPESA"
        }, {
          "name" : "matched",
          "in" : "query",
          "description" : "true: only payments tied to a subscriber. false: only unmatched ones.",
          "required" : false,
          "schema" : {
            "type" : "boolean"
          },
          "example" : true
        }, {
          "name" : "from",
          "in" : "query",
          "description" : "Received at or after this time (ISO 8601, UTC).",
          "required" : false,
          "schema" : {
            "type" : "string",
            "format" : "date-time"
          },
          "example" : "2026-10-01T00:00:00Z"
        }, {
          "name" : "to",
          "in" : "query",
          "description" : "Received at or before this time (ISO 8601, UTC).",
          "required" : false,
          "schema" : {
            "type" : "string",
            "format" : "date-time"
          },
          "example" : "2026-10-31T23:59:59Z"
        }, {
          "name" : "q",
          "in" : "query",
          "description" : "Search over receipt, phone, account reference and subscriber name.",
          "required" : false,
          "schema" : {
            "type" : "string"
          },
          "example" : "SJK3H7Y2QX"
        }, {
          "name" : "page",
          "in" : "query",
          "description" : "Page number, starting at 1.",
          "required" : false,
          "schema" : {
            "minimum" : 1,
            "type" : "integer",
            "format" : "int32",
            "default" : 1
          },
          "example" : 1
        }, {
          "name" : "size",
          "in" : "query",
          "description" : "Results per page, 1–200.",
          "required" : false,
          "schema" : {
            "maximum" : 200,
            "minimum" : 1,
            "type" : "integer",
            "format" : "int32",
            "default" : 50
          },
          "example" : 50
        }, {
          "$ref" : "#/components/parameters/TenantCode"
        } ],
        "responses" : {
          "200" : {
            "description" : "Success.",
            "headers" : {
              "X-RateLimit-Limit" : {
                "$ref" : "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining" : {
                "$ref" : "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content" : {
              "*/*" : {
                "schema" : {
                  "$ref" : "#/components/schemas/PagedResultPayment"
                }
              }
            }
          },
          "400" : {
            "description" : "The request is malformed or a parameter is invalid (codes: invalid_request, validation_failed, malformed_body, missing_parameter, invalid_parameter, tenant_required).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "401" : {
            "description" : "No API key, or the key is invalid, revoked or expired (codes: missing_api_key, invalid_api_key, api_key_revoked, api_key_expired).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "403" : {
            "description" : "The key lacks the scope (insufficient_scope), or the ISP's account is suspended or terminated.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "429" : {
            "description" : "Rate limit reached for this key (rate_limited). Wait Retry-After seconds.",
            "headers" : {
              "Retry-After" : {
                "$ref" : "#/components/headers/Retry-After"
              }
            },
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "500" : {
            "description" : "Unexpected error on our side (internal_error).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope" : "read",
        "x-required-permission" : [ "BILLING:PAYMENTS:VIEW" ],
        "x-changes-data" : false
      },
      "post" : {
        "tags" : [ "Payments" ],
        "summary" : "Record a payment for a subscriber",
        "description" : "Records money the ISP received outside CoreSync's own channels (cash, a bank transfer, an M-Pesa receipt) and applies it the same way an M-Pesa payment is applied: it goes to the subscriber's wallet, renews their subscription when it covers the plan price (and reactivates a suspended subscriber), and it appears in the ISP's payments and reports. Moves money: needs an Idempotency-Key, and a provider + reference is only ever recorded once, so passing the real receipt makes a duplicate impossible. Answers 201 when recorded, 200 when this exact receipt was already recorded for this subscriber.\n\nScope: `payments:write`. The key's creator must hold BILLING:PAYMENTS:EDIT.",
        "operationId" : "recordPayment",
        "parameters" : [ {
          "name" : "Idempotency-Key",
          "in" : "header",
          "description" : "A unique string per payment (e.g. a UUID). Retrying with the same key returns the first response instead of recording it again, for 24 hours.",
          "required" : true,
          "schema" : {
            "type" : "string"
          },
          "example" : "4d1c9f0e-7a2b-4f43-9a51-2f0c7c1d8e11"
        }, {
          "$ref" : "#/components/parameters/TenantCode"
        } ],
        "requestBody" : {
          "content" : {
            "application/json" : {
              "schema" : {
                "$ref" : "#/components/schemas/RecordPaymentRequest"
              }
            }
          },
          "required" : true
        },
        "responses" : {
          "201" : {
            "description" : "Recorded and applied.",
            "headers" : {
              "X-RateLimit-Limit" : {
                "$ref" : "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining" : {
                "$ref" : "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content" : {
              "*/*" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Payment"
                }
              }
            }
          },
          "200" : {
            "description" : "This receipt was already recorded for this subscriber; nothing changed.",
            "headers" : {
              "X-RateLimit-Limit" : {
                "$ref" : "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining" : {
                "$ref" : "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content" : {
              "*/*" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Payment"
                }
              }
            }
          },
          "400" : {
            "description" : "The request is malformed or a parameter is invalid (codes: invalid_request, validation_failed, malformed_body, missing_parameter, invalid_parameter, idempotency_key_required, tenant_required).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "401" : {
            "description" : "No API key, or the key is invalid, revoked or expired (codes: missing_api_key, invalid_api_key, api_key_revoked, api_key_expired).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "403" : {
            "description" : "The key lacks the scope (insufficient_scope), or the ISP's account is suspended or terminated.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "409" : {
            "description" : "Conflicts with the current state, e.g. idempotency_in_progress or duplicate_reference.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "422" : {
            "description" : "Refused by a business rule (e.g. invalid_state, same_plan, idempotency_key_reused); detail says why. Nothing was changed.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "429" : {
            "description" : "Rate limit reached for this key (rate_limited). Wait Retry-After seconds.",
            "headers" : {
              "Retry-After" : {
                "$ref" : "#/components/headers/Retry-After"
              }
            },
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "500" : {
            "description" : "Unexpected error on our side (internal_error).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope" : "payments:write",
        "x-required-permission" : [ "BILLING:PAYMENTS:EDIT" ],
        "x-changes-data" : true
      }
    },
    "/api/v1/migrations" : {
      "get" : {
        "tags" : [ "Migrations" ],
        "summary" : "List an ISP's migrations",
        "description" : "Lists the ISP's migration runs, newest first, with their status and counts. Changes nothing.\n\nPlatform keys only. Scope: `read`. The key's creator must hold SYSTEM:TENANTS:READ or SYSTEM:PLATFORM:MANAGE.",
        "operationId" : "listMigrations",
        "parameters" : [ {
          "name" : "page",
          "in" : "query",
          "description" : "Page number, starting at 1.",
          "required" : false,
          "schema" : {
            "minimum" : 1,
            "type" : "integer",
            "format" : "int32",
            "default" : 1
          },
          "example" : 1
        }, {
          "name" : "size",
          "in" : "query",
          "description" : "Results per page, 1–200.",
          "required" : false,
          "schema" : {
            "maximum" : 200,
            "minimum" : 1,
            "type" : "integer",
            "format" : "int32",
            "default" : 50
          },
          "example" : 50
        }, {
          "$ref" : "#/components/parameters/TenantCode"
        } ],
        "responses" : {
          "200" : {
            "description" : "Success.",
            "headers" : {
              "X-RateLimit-Limit" : {
                "$ref" : "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining" : {
                "$ref" : "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content" : {
              "*/*" : {
                "schema" : {
                  "$ref" : "#/components/schemas/PagedResultMigration"
                }
              }
            }
          },
          "400" : {
            "description" : "The request is malformed or a parameter is invalid (codes: invalid_request, validation_failed, malformed_body, missing_parameter, invalid_parameter, tenant_required).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "401" : {
            "description" : "No API key, or the key is invalid, revoked or expired (codes: missing_api_key, invalid_api_key, api_key_revoked, api_key_expired).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "403" : {
            "description" : "The key lacks the scope (insufficient_scope), or the ISP's account is suspended or terminated.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "429" : {
            "description" : "Rate limit reached for this key (rate_limited). Wait Retry-After seconds.",
            "headers" : {
              "Retry-After" : {
                "$ref" : "#/components/headers/Retry-After"
              }
            },
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "500" : {
            "description" : "Unexpected error on our side (internal_error).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-key-kind" : "PLATFORM",
        "x-required-scope" : "read",
        "x-required-permission" : [ "SYSTEM:TENANTS:READ", "SYSTEM:PLATFORM:MANAGE" ],
        "x-changes-data" : false
      },
      "post" : {
        "tags" : [ "Migrations" ],
        "summary" : "Upload an old system's export and see the migration plan",
        "description" : "Step 1 of a migration. Reads an export of the ISP's customers (CSV or Excel from Vinea and other systems, detected automatically) and proposes what to create: zones, plans with prices, speed profiles and router mappings. Nothing in the ISP's data changes yet; a migration record is created, so it needs an Idempotency-Key. Next: applyMigration. Blocked with 409 onboarding_gate until the ISP has agreed to the migration in onboarding.\n\nPlatform keys only. Scope: `migrations:write`. The key's creator must hold SYSTEM:PLATFORM:MANAGE.",
        "operationId" : "analyzeMigration",
        "parameters" : [ {
          "name" : "Idempotency-Key",
          "in" : "header",
          "description" : "A unique string per upload. Retrying with the same key returns the first response.",
          "required" : true,
          "schema" : {
            "type" : "string"
          },
          "example" : "3b1f6a2c-9d8e-4c7b-a6f5-0e1d2c3b4a59"
        }, {
          "$ref" : "#/components/parameters/TenantCode"
        } ],
        "requestBody" : {
          "content" : {
            "application/json" : {
              "schema" : {
                "$ref" : "#/components/schemas/AnalyzeMigrationRequest"
              }
            }
          },
          "required" : true
        },
        "responses" : {
          "201" : {
            "description" : "Analysed.",
            "headers" : {
              "X-RateLimit-Limit" : {
                "$ref" : "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining" : {
                "$ref" : "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content" : {
              "*/*" : {
                "schema" : {
                  "$ref" : "#/components/schemas/MigrationPlan"
                }
              }
            }
          },
          "400" : {
            "description" : "The request is malformed or a parameter is invalid (codes: invalid_request, validation_failed, malformed_body, missing_parameter, invalid_parameter, idempotency_key_required, tenant_required).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "401" : {
            "description" : "No API key, or the key is invalid, revoked or expired (codes: missing_api_key, invalid_api_key, api_key_revoked, api_key_expired).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "403" : {
            "description" : "The key lacks the scope (insufficient_scope), or the ISP's account is suspended or terminated.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "409" : {
            "description" : "Conflicts with the current state, e.g. idempotency_in_progress or duplicate_reference.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "422" : {
            "description" : "Refused by a business rule (e.g. invalid_state, same_plan, idempotency_key_reused); detail says why. Nothing was changed.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "429" : {
            "description" : "Rate limit reached for this key (rate_limited). Wait Retry-After seconds.",
            "headers" : {
              "Retry-After" : {
                "$ref" : "#/components/headers/Retry-After"
              }
            },
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "500" : {
            "description" : "Unexpected error on our side (internal_error).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-key-kind" : "PLATFORM",
        "x-required-scope" : "migrations:write",
        "x-required-permission" : [ "SYSTEM:PLATFORM:MANAGE" ],
        "x-changes-data" : true
      }
    },
    "/api/v1/migrations/{id}/verify" : {
      "post" : {
        "tags" : [ "Migrations" ],
        "summary" : "Check the import against the export",
        "description" : "Step 4. Compares every row of the export with what is now in CoreSync (present, plan, status, expiry, RADIUS login) and reports what is missing or differs. Records the finished import and the result on the run (VERIFIED when clean, otherwise it stays IMPORTED with the report); changes no subscriber.\n\nPlatform keys only. Scope: `migrations:write`. The key's creator must hold SYSTEM:PLATFORM:MANAGE.",
        "operationId" : "verifyMigration",
        "parameters" : [ {
          "name" : "id",
          "in" : "path",
          "description" : "Migration id.",
          "required" : true,
          "schema" : {
            "type" : "string"
          },
          "example" : "1a85f5e9-9c8a-4e7a-8a40-a1cd9a1c2706"
        }, {
          "$ref" : "#/components/parameters/TenantCode"
        } ],
        "responses" : {
          "200" : {
            "description" : "Success.",
            "headers" : {
              "X-RateLimit-Limit" : {
                "$ref" : "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining" : {
                "$ref" : "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content" : {
              "*/*" : {
                "schema" : {
                  "$ref" : "#/components/schemas/MigrationVerification"
                }
              }
            }
          },
          "400" : {
            "description" : "The request is malformed or a parameter is invalid (codes: invalid_request, validation_failed, malformed_body, missing_parameter, invalid_parameter, idempotency_key_required, tenant_required).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "401" : {
            "description" : "No API key, or the key is invalid, revoked or expired (codes: missing_api_key, invalid_api_key, api_key_revoked, api_key_expired).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "403" : {
            "description" : "The key lacks the scope (insufficient_scope), or the ISP's account is suspended or terminated.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "404" : {
            "description" : "No such record in this ISP's account (not_found). Records of other ISPs also answer 404.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "409" : {
            "description" : "Conflicts with the current state, e.g. idempotency_in_progress or duplicate_reference.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "422" : {
            "description" : "Refused by a business rule (e.g. invalid_state, same_plan, idempotency_key_reused); detail says why. Nothing was changed.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "429" : {
            "description" : "Rate limit reached for this key (rate_limited). Wait Retry-After seconds.",
            "headers" : {
              "Retry-After" : {
                "$ref" : "#/components/headers/Retry-After"
              }
            },
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "500" : {
            "description" : "Unexpected error on our side (internal_error).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-key-kind" : "PLATFORM",
        "x-required-scope" : "migrations:write",
        "x-required-permission" : [ "SYSTEM:PLATFORM:MANAGE" ],
        "x-changes-data" : true
      }
    },
    "/api/v1/migrations/{id}/import" : {
      "post" : {
        "tags" : [ "Migrations" ],
        "summary" : "Import the migrated customers as subscribers",
        "description" : "Step 3. Starts creating every prepared row as a CoreSync subscriber, with their plan, expiry and RADIUS login, in the background; poll getMigrationProgress. This creates real subscribers who can connect. Blocked with 409 onboarding_gate while the ISP's onboarding (agreement, platform payment) is incomplete.\n\nPlatform keys only. Scope: `migrations:write`. The key's creator must hold SYSTEM:PLATFORM:MANAGE.",
        "operationId" : "startMigrationImport",
        "parameters" : [ {
          "name" : "id",
          "in" : "path",
          "description" : "Migration id.",
          "required" : true,
          "schema" : {
            "type" : "string"
          },
          "example" : "1a85f5e9-9c8a-4e7a-8a40-a1cd9a1c2706"
        }, {
          "$ref" : "#/components/parameters/TenantCode"
        } ],
        "responses" : {
          "202" : {
            "description" : "Import started.",
            "headers" : {
              "X-RateLimit-Limit" : {
                "$ref" : "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining" : {
                "$ref" : "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content" : {
              "*/*" : {
                "schema" : {
                  "$ref" : "#/components/schemas/MigrationProgress"
                }
              }
            }
          },
          "400" : {
            "description" : "The request is malformed or a parameter is invalid (codes: invalid_request, validation_failed, malformed_body, missing_parameter, invalid_parameter, idempotency_key_required, tenant_required).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "401" : {
            "description" : "No API key, or the key is invalid, revoked or expired (codes: missing_api_key, invalid_api_key, api_key_revoked, api_key_expired).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "403" : {
            "description" : "The key lacks the scope (insufficient_scope), or the ISP's account is suspended or terminated.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "404" : {
            "description" : "No such record in this ISP's account (not_found). Records of other ISPs also answer 404.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "409" : {
            "description" : "Conflicts with the current state, e.g. idempotency_in_progress or duplicate_reference.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "422" : {
            "description" : "Refused by a business rule (e.g. invalid_state, same_plan, idempotency_key_reused); detail says why. Nothing was changed.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "429" : {
            "description" : "Rate limit reached for this key (rate_limited). Wait Retry-After seconds.",
            "headers" : {
              "Retry-After" : {
                "$ref" : "#/components/headers/Retry-After"
              }
            },
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "500" : {
            "description" : "Unexpected error on our side (internal_error).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-key-kind" : "PLATFORM",
        "x-required-scope" : "migrations:write",
        "x-required-permission" : [ "SYSTEM:PLATFORM:MANAGE" ],
        "x-changes-data" : true
      }
    },
    "/api/v1/migrations/{id}/apply" : {
      "post" : {
        "tags" : [ "Migrations" ],
        "summary" : "Apply a migration plan (creates zones and plans)",
        "description" : "Step 2. Creates the zones, buildings, speed profiles and plans the plan proposes (or reuses existing ones), using the suggested prices unless packages[] gives others; a package with price_required must be priced here or the call answers 422 price_required. Prepares the customer rows; no subscriber exists yet. Next: startMigrationImport.\n\nPlatform keys only. Scope: `migrations:write`. The key's creator must hold SYSTEM:PLATFORM:MANAGE.",
        "operationId" : "applyMigration",
        "parameters" : [ {
          "name" : "id",
          "in" : "path",
          "description" : "Migration id.",
          "required" : true,
          "schema" : {
            "type" : "string"
          },
          "example" : "1a85f5e9-9c8a-4e7a-8a40-a1cd9a1c2706"
        }, {
          "$ref" : "#/components/parameters/TenantCode"
        } ],
        "requestBody" : {
          "content" : {
            "application/json" : {
              "schema" : {
                "$ref" : "#/components/schemas/ApplyMigrationRequest"
              }
            }
          }
        },
        "responses" : {
          "200" : {
            "description" : "Success.",
            "headers" : {
              "X-RateLimit-Limit" : {
                "$ref" : "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining" : {
                "$ref" : "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content" : {
              "*/*" : {
                "schema" : {
                  "$ref" : "#/components/schemas/MigrationApplied"
                }
              }
            }
          },
          "400" : {
            "description" : "The request is malformed or a parameter is invalid (codes: invalid_request, validation_failed, malformed_body, missing_parameter, invalid_parameter, idempotency_key_required, tenant_required).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "401" : {
            "description" : "No API key, or the key is invalid, revoked or expired (codes: missing_api_key, invalid_api_key, api_key_revoked, api_key_expired).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "403" : {
            "description" : "The key lacks the scope (insufficient_scope), or the ISP's account is suspended or terminated.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "404" : {
            "description" : "No such record in this ISP's account (not_found). Records of other ISPs also answer 404.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "409" : {
            "description" : "Conflicts with the current state, e.g. idempotency_in_progress or duplicate_reference.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "422" : {
            "description" : "Refused by a business rule (e.g. invalid_state, same_plan, idempotency_key_reused); detail says why. Nothing was changed.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "429" : {
            "description" : "Rate limit reached for this key (rate_limited). Wait Retry-After seconds.",
            "headers" : {
              "Retry-After" : {
                "$ref" : "#/components/headers/Retry-After"
              }
            },
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "500" : {
            "description" : "Unexpected error on our side (internal_error).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-key-kind" : "PLATFORM",
        "x-required-scope" : "migrations:write",
        "x-required-permission" : [ "SYSTEM:PLATFORM:MANAGE" ],
        "x-changes-data" : true
      }
    },
    "/api/v1/webhooks/{id}" : {
      "get" : {
        "tags" : [ "Webhooks" ],
        "summary" : "Get a webhook endpoint",
        "description" : "Returns one endpoint and its delivery health (last success, last failure, whether it was switched off). Changes nothing.\n\nScope: `read`. The key's creator must hold SYSTEM:SETTINGS:VIEW.",
        "operationId" : "getWebhookEndpoint",
        "parameters" : [ {
          "name" : "id",
          "in" : "path",
          "description" : "Endpoint id.",
          "required" : true,
          "schema" : {
            "type" : "string",
            "format" : "uuid"
          },
          "example" : "2b3c4d5e-6f7a-8b9c-0d1e-2f3a4b5c6d7e"
        }, {
          "$ref" : "#/components/parameters/TenantCode"
        } ],
        "responses" : {
          "200" : {
            "description" : "Success.",
            "headers" : {
              "X-RateLimit-Limit" : {
                "$ref" : "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining" : {
                "$ref" : "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content" : {
              "*/*" : {
                "schema" : {
                  "$ref" : "#/components/schemas/WebhookEndpoint"
                }
              }
            }
          },
          "400" : {
            "description" : "The request is malformed or a parameter is invalid (codes: invalid_request, validation_failed, malformed_body, missing_parameter, invalid_parameter, tenant_required).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "401" : {
            "description" : "No API key, or the key is invalid, revoked or expired (codes: missing_api_key, invalid_api_key, api_key_revoked, api_key_expired).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "403" : {
            "description" : "The key lacks the scope (insufficient_scope), or the ISP's account is suspended or terminated.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "404" : {
            "description" : "No such record in this ISP's account (not_found). Records of other ISPs also answer 404.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "429" : {
            "description" : "Rate limit reached for this key (rate_limited). Wait Retry-After seconds.",
            "headers" : {
              "Retry-After" : {
                "$ref" : "#/components/headers/Retry-After"
              }
            },
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "500" : {
            "description" : "Unexpected error on our side (internal_error).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope" : "read",
        "x-required-permission" : [ "SYSTEM:SETTINGS:VIEW" ],
        "x-changes-data" : false
      },
      "delete" : {
        "tags" : [ "Webhooks" ],
        "summary" : "Delete a webhook endpoint",
        "description" : "Deletes the endpoint and its delivery log. Events stop going to it at once; anything still queued is dropped.\n\nScope: `webhooks:write`. The key's creator must hold SYSTEM:SETTINGS:EDIT.",
        "operationId" : "deleteWebhookEndpoint",
        "parameters" : [ {
          "name" : "id",
          "in" : "path",
          "description" : "Endpoint id.",
          "required" : true,
          "schema" : {
            "type" : "string",
            "format" : "uuid"
          },
          "example" : "2b3c4d5e-6f7a-8b9c-0d1e-2f3a4b5c6d7e"
        }, {
          "$ref" : "#/components/parameters/TenantCode"
        } ],
        "responses" : {
          "204" : {
            "description" : "Deleted.",
            "headers" : {
              "X-RateLimit-Limit" : {
                "$ref" : "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining" : {
                "$ref" : "#/components/headers/X-RateLimit-Remaining"
              }
            }
          },
          "400" : {
            "description" : "The request is malformed or a parameter is invalid (codes: invalid_request, validation_failed, malformed_body, missing_parameter, invalid_parameter, idempotency_key_required, tenant_required).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "401" : {
            "description" : "No API key, or the key is invalid, revoked or expired (codes: missing_api_key, invalid_api_key, api_key_revoked, api_key_expired).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "403" : {
            "description" : "The key lacks the scope (insufficient_scope), or the ISP's account is suspended or terminated.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "404" : {
            "description" : "No such record in this ISP's account (not_found). Records of other ISPs also answer 404.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "409" : {
            "description" : "Conflicts with the current state, e.g. idempotency_in_progress or duplicate_reference.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "422" : {
            "description" : "Refused by a business rule (e.g. invalid_state, same_plan, idempotency_key_reused); detail says why. Nothing was changed.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "429" : {
            "description" : "Rate limit reached for this key (rate_limited). Wait Retry-After seconds.",
            "headers" : {
              "Retry-After" : {
                "$ref" : "#/components/headers/Retry-After"
              }
            },
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "500" : {
            "description" : "Unexpected error on our side (internal_error).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope" : "webhooks:write",
        "x-required-permission" : [ "SYSTEM:SETTINGS:EDIT" ],
        "x-changes-data" : true
      },
      "patch" : {
        "tags" : [ "Webhooks" ],
        "summary" : "Change a webhook endpoint",
        "description" : "Changes the URL, the events, the note, or switches the endpoint off or back on (which also clears an automatic switch-off). Only the fields sent change. The signing secret stays the same.\n\nScope: `webhooks:write`. The key's creator must hold SYSTEM:SETTINGS:EDIT.",
        "operationId" : "updateWebhookEndpoint",
        "parameters" : [ {
          "name" : "id",
          "in" : "path",
          "description" : "Endpoint id.",
          "required" : true,
          "schema" : {
            "type" : "string",
            "format" : "uuid"
          },
          "example" : "2b3c4d5e-6f7a-8b9c-0d1e-2f3a4b5c6d7e"
        }, {
          "$ref" : "#/components/parameters/TenantCode"
        } ],
        "requestBody" : {
          "content" : {
            "application/json" : {
              "schema" : {
                "$ref" : "#/components/schemas/UpdateWebhookEndpointRequest"
              }
            }
          },
          "required" : true
        },
        "responses" : {
          "200" : {
            "description" : "Success.",
            "headers" : {
              "X-RateLimit-Limit" : {
                "$ref" : "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining" : {
                "$ref" : "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content" : {
              "*/*" : {
                "schema" : {
                  "$ref" : "#/components/schemas/WebhookEndpoint"
                }
              }
            }
          },
          "400" : {
            "description" : "The request is malformed or a parameter is invalid (codes: invalid_request, validation_failed, malformed_body, missing_parameter, invalid_parameter, idempotency_key_required, tenant_required).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "401" : {
            "description" : "No API key, or the key is invalid, revoked or expired (codes: missing_api_key, invalid_api_key, api_key_revoked, api_key_expired).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "403" : {
            "description" : "The key lacks the scope (insufficient_scope), or the ISP's account is suspended or terminated.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "404" : {
            "description" : "No such record in this ISP's account (not_found). Records of other ISPs also answer 404.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "409" : {
            "description" : "Conflicts with the current state, e.g. idempotency_in_progress or duplicate_reference.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "422" : {
            "description" : "Refused by a business rule (e.g. invalid_state, same_plan, idempotency_key_reused); detail says why. Nothing was changed.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "429" : {
            "description" : "Rate limit reached for this key (rate_limited). Wait Retry-After seconds.",
            "headers" : {
              "Retry-After" : {
                "$ref" : "#/components/headers/Retry-After"
              }
            },
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "500" : {
            "description" : "Unexpected error on our side (internal_error).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope" : "webhooks:write",
        "x-required-permission" : [ "SYSTEM:SETTINGS:EDIT" ],
        "x-changes-data" : true
      }
    },
    "/api/v1/webhooks/{id}/deliveries" : {
      "get" : {
        "tags" : [ "Webhooks" ],
        "summary" : "List an endpoint's deliveries",
        "description" : "The delivery log for one endpoint, newest first: each event, its status, attempts, the HTTP status the endpoint answered and the last error. Kept for 30 days. Changes nothing.\n\nScope: `read`. The key's creator must hold SYSTEM:SETTINGS:VIEW.",
        "operationId" : "listWebhookDeliveries",
        "parameters" : [ {
          "name" : "id",
          "in" : "path",
          "description" : "Endpoint id.",
          "required" : true,
          "schema" : {
            "type" : "string",
            "format" : "uuid"
          },
          "example" : "2b3c4d5e-6f7a-8b9c-0d1e-2f3a4b5c6d7e"
        }, {
          "name" : "page",
          "in" : "query",
          "description" : "Page number, starting at 1.",
          "required" : false,
          "schema" : {
            "minimum" : 1,
            "type" : "integer",
            "format" : "int32",
            "default" : 1
          },
          "example" : 1
        }, {
          "name" : "size",
          "in" : "query",
          "description" : "Results per page, 1–200.",
          "required" : false,
          "schema" : {
            "maximum" : 200,
            "minimum" : 1,
            "type" : "integer",
            "format" : "int32",
            "default" : 50
          },
          "example" : 50
        }, {
          "$ref" : "#/components/parameters/TenantCode"
        } ],
        "responses" : {
          "200" : {
            "description" : "Success.",
            "headers" : {
              "X-RateLimit-Limit" : {
                "$ref" : "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining" : {
                "$ref" : "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content" : {
              "*/*" : {
                "schema" : {
                  "$ref" : "#/components/schemas/PagedResultWebhookDelivery"
                }
              }
            }
          },
          "400" : {
            "description" : "The request is malformed or a parameter is invalid (codes: invalid_request, validation_failed, malformed_body, missing_parameter, invalid_parameter, tenant_required).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "401" : {
            "description" : "No API key, or the key is invalid, revoked or expired (codes: missing_api_key, invalid_api_key, api_key_revoked, api_key_expired).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "403" : {
            "description" : "The key lacks the scope (insufficient_scope), or the ISP's account is suspended or terminated.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "404" : {
            "description" : "No such record in this ISP's account (not_found). Records of other ISPs also answer 404.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "429" : {
            "description" : "Rate limit reached for this key (rate_limited). Wait Retry-After seconds.",
            "headers" : {
              "Retry-After" : {
                "$ref" : "#/components/headers/Retry-After"
              }
            },
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "500" : {
            "description" : "Unexpected error on our side (internal_error).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope" : "read",
        "x-required-permission" : [ "SYSTEM:SETTINGS:VIEW" ],
        "x-changes-data" : false
      }
    },
    "/api/v1/tenants/{code}" : {
      "get" : {
        "tags" : [ "Platform" ],
        "summary" : "Get an ISP (platform keys only)",
        "description" : "Returns one ISP by its code: name, status, currency, country and time zone. Changes nothing.\n\nPlatform keys only. Scope: `read`. The key's creator must hold SYSTEM:TENANTS:READ or SYSTEM:PLATFORM:MANAGE.",
        "operationId" : "getTenant",
        "parameters" : [ {
          "name" : "code",
          "in" : "path",
          "description" : "The ISP's code.",
          "required" : true,
          "schema" : {
            "type" : "string"
          },
          "example" : "SKYNET"
        }, {
          "$ref" : "#/components/parameters/TenantCode"
        } ],
        "responses" : {
          "200" : {
            "description" : "Success.",
            "headers" : {
              "X-RateLimit-Limit" : {
                "$ref" : "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining" : {
                "$ref" : "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content" : {
              "*/*" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Tenant"
                }
              }
            }
          },
          "400" : {
            "description" : "The request is malformed or a parameter is invalid (codes: invalid_request, validation_failed, malformed_body, missing_parameter, invalid_parameter, tenant_required).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "401" : {
            "description" : "No API key, or the key is invalid, revoked or expired (codes: missing_api_key, invalid_api_key, api_key_revoked, api_key_expired).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "403" : {
            "description" : "The key lacks the scope (insufficient_scope), or the ISP's account is suspended or terminated.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "404" : {
            "description" : "No such record in this ISP's account (not_found). Records of other ISPs also answer 404.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "429" : {
            "description" : "Rate limit reached for this key (rate_limited). Wait Retry-After seconds.",
            "headers" : {
              "Retry-After" : {
                "$ref" : "#/components/headers/Retry-After"
              }
            },
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "500" : {
            "description" : "Unexpected error on our side (internal_error).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-key-kind" : "PLATFORM",
        "x-required-scope" : "read",
        "x-required-permission" : [ "SYSTEM:TENANTS:READ", "SYSTEM:PLATFORM:MANAGE" ],
        "x-changes-data" : false
      }
    },
    "/api/v1/subscribers" : {
      "get" : {
        "tags" : [ "Subscribers" ],
        "summary" : "List subscribers",
        "description" : "Lists the ISP's subscribers, newest first, optionally filtered by status or a free-text search over name, username, account number, paybill code, email and phone. To find one person by an exact phone, email or account number, use lookupSubscribers instead. Changes nothing.\n\nScope: `read`. The key's creator must hold BILLING:SUBSCRIBERS:VIEW.",
        "operationId" : "listSubscribers",
        "parameters" : [ {
          "name" : "status",
          "in" : "query",
          "description" : "Only subscribers in this state. ONLINE / OFFLINE filter on the live connection.",
          "required" : false,
          "schema" : {
            "type" : "string",
            "enum" : [ "ACTIVE", "SUSPENDED", "PENDING", "DRAFT", "TERMINATED", "ONLINE", "OFFLINE" ]
          }
        }, {
          "name" : "q",
          "in" : "query",
          "description" : "Free-text search (partial matches).",
          "required" : false,
          "schema" : {
            "type" : "string"
          },
          "example" : "wanjiru"
        }, {
          "name" : "page",
          "in" : "query",
          "description" : "Page number, starting at 1.",
          "required" : false,
          "schema" : {
            "minimum" : 1,
            "type" : "integer",
            "format" : "int32",
            "default" : 1
          },
          "example" : 1
        }, {
          "name" : "size",
          "in" : "query",
          "description" : "Results per page, 1–200.",
          "required" : false,
          "schema" : {
            "maximum" : 200,
            "minimum" : 1,
            "type" : "integer",
            "format" : "int32",
            "default" : 50
          },
          "example" : 50
        }, {
          "$ref" : "#/components/parameters/TenantCode"
        } ],
        "responses" : {
          "200" : {
            "description" : "Success.",
            "headers" : {
              "X-RateLimit-Limit" : {
                "$ref" : "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining" : {
                "$ref" : "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content" : {
              "*/*" : {
                "schema" : {
                  "$ref" : "#/components/schemas/PagedResultSubscriberSummary"
                }
              }
            }
          },
          "400" : {
            "description" : "The request is malformed or a parameter is invalid (codes: invalid_request, validation_failed, malformed_body, missing_parameter, invalid_parameter, tenant_required).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "401" : {
            "description" : "No API key, or the key is invalid, revoked or expired (codes: missing_api_key, invalid_api_key, api_key_revoked, api_key_expired).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "403" : {
            "description" : "The key lacks the scope (insufficient_scope), or the ISP's account is suspended or terminated.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "429" : {
            "description" : "Rate limit reached for this key (rate_limited). Wait Retry-After seconds.",
            "headers" : {
              "Retry-After" : {
                "$ref" : "#/components/headers/Retry-After"
              }
            },
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "500" : {
            "description" : "Unexpected error on our side (internal_error).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope" : "read",
        "x-required-permission" : [ "BILLING:SUBSCRIBERS:VIEW" ],
        "x-changes-data" : false
      }
    },
    "/api/v1/subscribers/{id}" : {
      "get" : {
        "tags" : [ "Subscribers" ],
        "summary" : "Get a subscriber",
        "description" : "Returns one subscriber with their status, current plan and subscription (expiry), balance and whether they are online. Changes nothing.\n\nScope: `read`. The key's creator must hold BILLING:SUBSCRIBERS:VIEW.",
        "operationId" : "getSubscriber",
        "parameters" : [ {
          "name" : "id",
          "in" : "path",
          "description" : "Subscriber id.",
          "required" : true,
          "schema" : {
            "type" : "string",
            "format" : "uuid"
          },
          "example" : "3f2a6c1e-8b7d-4c1a-9e2f-1a2b3c4d5e6f"
        }, {
          "$ref" : "#/components/parameters/TenantCode"
        } ],
        "responses" : {
          "200" : {
            "description" : "Success.",
            "headers" : {
              "X-RateLimit-Limit" : {
                "$ref" : "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining" : {
                "$ref" : "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content" : {
              "*/*" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Subscriber"
                }
              }
            }
          },
          "400" : {
            "description" : "The request is malformed or a parameter is invalid (codes: invalid_request, validation_failed, malformed_body, missing_parameter, invalid_parameter, tenant_required).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "401" : {
            "description" : "No API key, or the key is invalid, revoked or expired (codes: missing_api_key, invalid_api_key, api_key_revoked, api_key_expired).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "403" : {
            "description" : "The key lacks the scope (insufficient_scope), or the ISP's account is suspended or terminated.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "404" : {
            "description" : "No such record in this ISP's account (not_found). Records of other ISPs also answer 404.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "429" : {
            "description" : "Rate limit reached for this key (rate_limited). Wait Retry-After seconds.",
            "headers" : {
              "Retry-After" : {
                "$ref" : "#/components/headers/Retry-After"
              }
            },
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "500" : {
            "description" : "Unexpected error on our side (internal_error).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope" : "read",
        "x-required-permission" : [ "BILLING:SUBSCRIBERS:VIEW" ],
        "x-changes-data" : false
      }
    },
    "/api/v1/subscribers/{id}/session" : {
      "get" : {
        "tags" : [ "Subscribers" ],
        "summary" : "Get a subscriber's connection",
        "description" : "Says whether the subscriber is online now, with the live RADIUS session (IP, router, since when, data used) and their last ended session (when and why it dropped). Use it for \"is my internet on\" or \"when did it drop\". Changes nothing.\n\nScope: `read`. The key's creator must hold SYSTEM:DASHBOARD:VIEW or BILLING:SUBSCRIBERS:READ.",
        "operationId" : "getSubscriberSession",
        "parameters" : [ {
          "name" : "id",
          "in" : "path",
          "description" : "Subscriber id.",
          "required" : true,
          "schema" : {
            "type" : "string",
            "format" : "uuid"
          },
          "example" : "3f2a6c1e-8b7d-4c1a-9e2f-1a2b3c4d5e6f"
        }, {
          "$ref" : "#/components/parameters/TenantCode"
        } ],
        "responses" : {
          "200" : {
            "description" : "Success.",
            "headers" : {
              "X-RateLimit-Limit" : {
                "$ref" : "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining" : {
                "$ref" : "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content" : {
              "*/*" : {
                "schema" : {
                  "$ref" : "#/components/schemas/SubscriberSession"
                }
              }
            }
          },
          "400" : {
            "description" : "The request is malformed or a parameter is invalid (codes: invalid_request, validation_failed, malformed_body, missing_parameter, invalid_parameter, tenant_required).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "401" : {
            "description" : "No API key, or the key is invalid, revoked or expired (codes: missing_api_key, invalid_api_key, api_key_revoked, api_key_expired).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "403" : {
            "description" : "The key lacks the scope (insufficient_scope), or the ISP's account is suspended or terminated.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "404" : {
            "description" : "No such record in this ISP's account (not_found). Records of other ISPs also answer 404.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "429" : {
            "description" : "Rate limit reached for this key (rate_limited). Wait Retry-After seconds.",
            "headers" : {
              "Retry-After" : {
                "$ref" : "#/components/headers/Retry-After"
              }
            },
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "500" : {
            "description" : "Unexpected error on our side (internal_error).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope" : "read",
        "x-required-permission" : [ "SYSTEM:DASHBOARD:VIEW", "BILLING:SUBSCRIBERS:READ" ],
        "x-changes-data" : false
      }
    },
    "/api/v1/subscribers/{id}/payments" : {
      "get" : {
        "tags" : [ "Subscribers" ],
        "summary" : "List a subscriber's payments",
        "description" : "Lists the payments applied to one subscriber, newest first. Changes nothing.\n\nScope: `read`. The key's creator must hold BILLING:PAYMENTS:VIEW.",
        "operationId" : "listSubscriberPayments",
        "parameters" : [ {
          "name" : "id",
          "in" : "path",
          "description" : "Subscriber id.",
          "required" : true,
          "schema" : {
            "type" : "string",
            "format" : "uuid"
          },
          "example" : "3f2a6c1e-8b7d-4c1a-9e2f-1a2b3c4d5e6f"
        }, {
          "name" : "page",
          "in" : "query",
          "description" : "Page number, starting at 1.",
          "required" : false,
          "schema" : {
            "minimum" : 1,
            "type" : "integer",
            "format" : "int32",
            "default" : 1
          },
          "example" : 1
        }, {
          "name" : "size",
          "in" : "query",
          "description" : "Results per page, 1–200.",
          "required" : false,
          "schema" : {
            "maximum" : 200,
            "minimum" : 1,
            "type" : "integer",
            "format" : "int32",
            "default" : 50
          },
          "example" : 50
        }, {
          "$ref" : "#/components/parameters/TenantCode"
        } ],
        "responses" : {
          "200" : {
            "description" : "Success.",
            "headers" : {
              "X-RateLimit-Limit" : {
                "$ref" : "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining" : {
                "$ref" : "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content" : {
              "*/*" : {
                "schema" : {
                  "$ref" : "#/components/schemas/PagedResultPayment"
                }
              }
            }
          },
          "400" : {
            "description" : "The request is malformed or a parameter is invalid (codes: invalid_request, validation_failed, malformed_body, missing_parameter, invalid_parameter, tenant_required).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "401" : {
            "description" : "No API key, or the key is invalid, revoked or expired (codes: missing_api_key, invalid_api_key, api_key_revoked, api_key_expired).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "403" : {
            "description" : "The key lacks the scope (insufficient_scope), or the ISP's account is suspended or terminated.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "404" : {
            "description" : "No such record in this ISP's account (not_found). Records of other ISPs also answer 404.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "429" : {
            "description" : "Rate limit reached for this key (rate_limited). Wait Retry-After seconds.",
            "headers" : {
              "Retry-After" : {
                "$ref" : "#/components/headers/Retry-After"
              }
            },
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "500" : {
            "description" : "Unexpected error on our side (internal_error).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope" : "read",
        "x-required-permission" : [ "BILLING:PAYMENTS:VIEW" ],
        "x-changes-data" : false
      }
    },
    "/api/v1/subscribers/{id}/invoices" : {
      "get" : {
        "tags" : [ "Subscribers" ],
        "summary" : "List a subscriber's invoices",
        "description" : "Lists the invoices issued to one subscriber, newest first. Changes nothing.\n\nScope: `read`. The key's creator must hold BILLING:SUBSCRIBERS:VIEW.",
        "operationId" : "listSubscriberInvoices",
        "parameters" : [ {
          "name" : "id",
          "in" : "path",
          "description" : "Subscriber id.",
          "required" : true,
          "schema" : {
            "type" : "string",
            "format" : "uuid"
          },
          "example" : "3f2a6c1e-8b7d-4c1a-9e2f-1a2b3c4d5e6f"
        }, {
          "name" : "page",
          "in" : "query",
          "description" : "Page number, starting at 1.",
          "required" : false,
          "schema" : {
            "minimum" : 1,
            "type" : "integer",
            "format" : "int32",
            "default" : 1
          },
          "example" : 1
        }, {
          "name" : "size",
          "in" : "query",
          "description" : "Results per page, 1–200.",
          "required" : false,
          "schema" : {
            "maximum" : 200,
            "minimum" : 1,
            "type" : "integer",
            "format" : "int32",
            "default" : 50
          },
          "example" : 50
        }, {
          "$ref" : "#/components/parameters/TenantCode"
        } ],
        "responses" : {
          "200" : {
            "description" : "Success.",
            "headers" : {
              "X-RateLimit-Limit" : {
                "$ref" : "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining" : {
                "$ref" : "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content" : {
              "*/*" : {
                "schema" : {
                  "$ref" : "#/components/schemas/PagedResultInvoice"
                }
              }
            }
          },
          "400" : {
            "description" : "The request is malformed or a parameter is invalid (codes: invalid_request, validation_failed, malformed_body, missing_parameter, invalid_parameter, tenant_required).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "401" : {
            "description" : "No API key, or the key is invalid, revoked or expired (codes: missing_api_key, invalid_api_key, api_key_revoked, api_key_expired).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "403" : {
            "description" : "The key lacks the scope (insufficient_scope), or the ISP's account is suspended or terminated.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "404" : {
            "description" : "No such record in this ISP's account (not_found). Records of other ISPs also answer 404.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "429" : {
            "description" : "Rate limit reached for this key (rate_limited). Wait Retry-After seconds.",
            "headers" : {
              "Retry-After" : {
                "$ref" : "#/components/headers/Retry-After"
              }
            },
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "500" : {
            "description" : "Unexpected error on our side (internal_error).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope" : "read",
        "x-required-permission" : [ "BILLING:SUBSCRIBERS:VIEW" ],
        "x-changes-data" : false
      }
    },
    "/api/v1/subscribers/{id}/balance" : {
      "get" : {
        "tags" : [ "Subscribers" ],
        "summary" : "Get what a subscriber has paid ahead and owes",
        "description" : "Returns the subscriber's wallet credit, the total of their unpaid invoices, what the next cycle costs and when the current one ends. Use it to answer \"how much do I owe\" or \"when does my internet expire\". Changes nothing.\n\nScope: `read`. The key's creator must hold BILLING:SUBSCRIBERS:VIEW.",
        "operationId" : "getSubscriberBalance",
        "parameters" : [ {
          "name" : "id",
          "in" : "path",
          "description" : "Subscriber id.",
          "required" : true,
          "schema" : {
            "type" : "string",
            "format" : "uuid"
          },
          "example" : "3f2a6c1e-8b7d-4c1a-9e2f-1a2b3c4d5e6f"
        }, {
          "$ref" : "#/components/parameters/TenantCode"
        } ],
        "responses" : {
          "200" : {
            "description" : "Success.",
            "headers" : {
              "X-RateLimit-Limit" : {
                "$ref" : "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining" : {
                "$ref" : "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content" : {
              "*/*" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Balance"
                }
              }
            }
          },
          "400" : {
            "description" : "The request is malformed or a parameter is invalid (codes: invalid_request, validation_failed, malformed_body, missing_parameter, invalid_parameter, tenant_required).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "401" : {
            "description" : "No API key, or the key is invalid, revoked or expired (codes: missing_api_key, invalid_api_key, api_key_revoked, api_key_expired).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "403" : {
            "description" : "The key lacks the scope (insufficient_scope), or the ISP's account is suspended or terminated.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "404" : {
            "description" : "No such record in this ISP's account (not_found). Records of other ISPs also answer 404.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "429" : {
            "description" : "Rate limit reached for this key (rate_limited). Wait Retry-After seconds.",
            "headers" : {
              "Retry-After" : {
                "$ref" : "#/components/headers/Retry-After"
              }
            },
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "500" : {
            "description" : "Unexpected error on our side (internal_error).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope" : "read",
        "x-required-permission" : [ "BILLING:SUBSCRIBERS:VIEW" ],
        "x-changes-data" : false
      }
    },
    "/api/v1/subscribers/lookup" : {
      "get" : {
        "tags" : [ "Subscribers" ],
        "summary" : "Find subscribers by phone, email or account number",
        "description" : "Finds subscribers by exactly one identifier. phone accepts any format (0712345678, 254712345678, +254 712 345 678) and matches however the number is stored; email ignores case; account matches the account number, the paybill code or the PPPoE username exactly. Usually returns one subscriber, but one phone can belong to several accounts. Use it to match a caller or a customer from another system. Changes nothing.\n\nScope: `read`. The key's creator must hold BILLING:SUBSCRIBERS:VIEW.",
        "operationId" : "lookupSubscribers",
        "parameters" : [ {
          "name" : "phone",
          "in" : "query",
          "description" : "Phone number in any common format; compared as E.164.",
          "required" : false,
          "schema" : {
            "type" : "string"
          },
          "example" : "+254712345678"
        }, {
          "name" : "email",
          "in" : "query",
          "description" : "Email address (case-insensitive, exact).",
          "required" : false,
          "schema" : {
            "type" : "string"
          },
          "example" : "jane@example.com"
        }, {
          "name" : "account",
          "in" : "query",
          "description" : "Account number, paybill code or PPPoE username (exact).",
          "required" : false,
          "schema" : {
            "type" : "string"
          },
          "example" : "ACC-00123"
        }, {
          "name" : "page",
          "in" : "query",
          "description" : "Page number, starting at 1.",
          "required" : false,
          "schema" : {
            "minimum" : 1,
            "type" : "integer",
            "format" : "int32",
            "default" : 1
          },
          "example" : 1
        }, {
          "name" : "size",
          "in" : "query",
          "description" : "Results per page, 1–200.",
          "required" : false,
          "schema" : {
            "maximum" : 200,
            "minimum" : 1,
            "type" : "integer",
            "format" : "int32",
            "default" : 50
          },
          "example" : 50
        }, {
          "$ref" : "#/components/parameters/TenantCode"
        } ],
        "responses" : {
          "200" : {
            "description" : "Success.",
            "headers" : {
              "X-RateLimit-Limit" : {
                "$ref" : "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining" : {
                "$ref" : "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content" : {
              "*/*" : {
                "schema" : {
                  "$ref" : "#/components/schemas/PagedResultSubscriberSummary"
                }
              }
            }
          },
          "400" : {
            "description" : "The request is malformed or a parameter is invalid (codes: invalid_request, validation_failed, malformed_body, missing_parameter, invalid_parameter, tenant_required).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "401" : {
            "description" : "No API key, or the key is invalid, revoked or expired (codes: missing_api_key, invalid_api_key, api_key_revoked, api_key_expired).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "403" : {
            "description" : "The key lacks the scope (insufficient_scope), or the ISP's account is suspended or terminated.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "429" : {
            "description" : "Rate limit reached for this key (rate_limited). Wait Retry-After seconds.",
            "headers" : {
              "Retry-After" : {
                "$ref" : "#/components/headers/Retry-After"
              }
            },
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "500" : {
            "description" : "Unexpected error on our side (internal_error).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope" : "read",
        "x-required-permission" : [ "BILLING:SUBSCRIBERS:VIEW" ],
        "x-changes-data" : false
      }
    },
    "/api/v1/sessions" : {
      "get" : {
        "tags" : [ "Sessions" ],
        "summary" : "List who is online now",
        "description" : "Lists live RADIUS sessions on the ISP's routers, newest first: PPPoE subscribers and hotspot devices, with IP, MAC, router, start time and data used. total is the number online. For one subscriber use getSubscriberSession. Changes nothing.\n\nScope: `read`. The key's creator must hold SYSTEM:DASHBOARD:VIEW or BILLING:SUBSCRIBERS:READ.",
        "operationId" : "listOnlineSessions",
        "parameters" : [ {
          "name" : "type",
          "in" : "query",
          "description" : "Only PPPoE subscribers or only hotspot devices.",
          "required" : false,
          "schema" : {
            "type" : "string",
            "enum" : [ "PPPOE", "HOTSPOT" ]
          }
        }, {
          "name" : "q",
          "in" : "query",
          "description" : "Search over username, MAC and IP (partial).",
          "required" : false,
          "schema" : {
            "type" : "string"
          },
          "example" : "jane"
        }, {
          "name" : "page",
          "in" : "query",
          "description" : "Page number, starting at 1.",
          "required" : false,
          "schema" : {
            "minimum" : 1,
            "type" : "integer",
            "format" : "int32",
            "default" : 1
          },
          "example" : 1
        }, {
          "name" : "size",
          "in" : "query",
          "description" : "Results per page, 1–200.",
          "required" : false,
          "schema" : {
            "maximum" : 200,
            "minimum" : 1,
            "type" : "integer",
            "format" : "int32",
            "default" : 50
          },
          "example" : 50
        }, {
          "$ref" : "#/components/parameters/TenantCode"
        } ],
        "responses" : {
          "200" : {
            "description" : "Success.",
            "headers" : {
              "X-RateLimit-Limit" : {
                "$ref" : "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining" : {
                "$ref" : "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content" : {
              "*/*" : {
                "schema" : {
                  "$ref" : "#/components/schemas/PagedResultSession"
                }
              }
            }
          },
          "400" : {
            "description" : "The request is malformed or a parameter is invalid (codes: invalid_request, validation_failed, malformed_body, missing_parameter, invalid_parameter, tenant_required).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "401" : {
            "description" : "No API key, or the key is invalid, revoked or expired (codes: missing_api_key, invalid_api_key, api_key_revoked, api_key_expired).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "403" : {
            "description" : "The key lacks the scope (insufficient_scope), or the ISP's account is suspended or terminated.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "429" : {
            "description" : "Rate limit reached for this key (rate_limited). Wait Retry-After seconds.",
            "headers" : {
              "Retry-After" : {
                "$ref" : "#/components/headers/Retry-After"
              }
            },
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "500" : {
            "description" : "Unexpected error on our side (internal_error).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope" : "read",
        "x-required-permission" : [ "SYSTEM:DASHBOARD:VIEW", "BILLING:SUBSCRIBERS:READ" ],
        "x-changes-data" : false
      }
    },
    "/api/v1/plans" : {
      "get" : {
        "tags" : [ "Plans" ],
        "summary" : "List plans",
        "description" : "Lists the ISP's service plans with price, cycle length and speed, sorted by name. Use it to answer \"what packages do you have\" or to find the plan_id for changeSubscriberPlan. Changes nothing.\n\nScope: `read`. The key's creator must hold BILLING:PLANS:VIEW.",
        "operationId" : "listPlans",
        "parameters" : [ {
          "name" : "status",
          "in" : "query",
          "description" : "Only plans with this status.",
          "required" : false,
          "schema" : {
            "type" : "string",
            "enum" : [ "ACTIVE", "INACTIVE", "ARCHIVED" ]
          }
        }, {
          "name" : "type",
          "in" : "query",
          "description" : "FIXED (home/business lines) or HOTSPOT (WiFi vouchers).",
          "required" : false,
          "schema" : {
            "type" : "string",
            "enum" : [ "FIXED", "HOTSPOT" ]
          }
        }, {
          "name" : "page",
          "in" : "query",
          "description" : "Page number, starting at 1.",
          "required" : false,
          "schema" : {
            "minimum" : 1,
            "type" : "integer",
            "format" : "int32",
            "default" : 1
          },
          "example" : 1
        }, {
          "name" : "size",
          "in" : "query",
          "description" : "Results per page, 1–200.",
          "required" : false,
          "schema" : {
            "maximum" : 200,
            "minimum" : 1,
            "type" : "integer",
            "format" : "int32",
            "default" : 50
          },
          "example" : 50
        }, {
          "$ref" : "#/components/parameters/TenantCode"
        } ],
        "responses" : {
          "200" : {
            "description" : "Success.",
            "headers" : {
              "X-RateLimit-Limit" : {
                "$ref" : "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining" : {
                "$ref" : "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content" : {
              "*/*" : {
                "schema" : {
                  "$ref" : "#/components/schemas/PagedResultPlan"
                }
              }
            }
          },
          "400" : {
            "description" : "The request is malformed or a parameter is invalid (codes: invalid_request, validation_failed, malformed_body, missing_parameter, invalid_parameter, tenant_required).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "401" : {
            "description" : "No API key, or the key is invalid, revoked or expired (codes: missing_api_key, invalid_api_key, api_key_revoked, api_key_expired).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "403" : {
            "description" : "The key lacks the scope (insufficient_scope), or the ISP's account is suspended or terminated.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "429" : {
            "description" : "Rate limit reached for this key (rate_limited). Wait Retry-After seconds.",
            "headers" : {
              "Retry-After" : {
                "$ref" : "#/components/headers/Retry-After"
              }
            },
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "500" : {
            "description" : "Unexpected error on our side (internal_error).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope" : "read",
        "x-required-permission" : [ "BILLING:PLANS:VIEW" ],
        "x-changes-data" : false
      }
    },
    "/api/v1/plans/{id}" : {
      "get" : {
        "tags" : [ "Plans" ],
        "summary" : "Get a plan",
        "description" : "Returns one service plan. Changes nothing.\n\nScope: `read`. The key's creator must hold BILLING:PLANS:VIEW.",
        "operationId" : "getPlan",
        "parameters" : [ {
          "name" : "id",
          "in" : "path",
          "description" : "Plan id.",
          "required" : true,
          "schema" : {
            "type" : "string",
            "format" : "uuid"
          },
          "example" : "9c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f"
        }, {
          "$ref" : "#/components/parameters/TenantCode"
        } ],
        "responses" : {
          "200" : {
            "description" : "Success.",
            "headers" : {
              "X-RateLimit-Limit" : {
                "$ref" : "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining" : {
                "$ref" : "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content" : {
              "*/*" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Plan"
                }
              }
            }
          },
          "400" : {
            "description" : "The request is malformed or a parameter is invalid (codes: invalid_request, validation_failed, malformed_body, missing_parameter, invalid_parameter, tenant_required).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "401" : {
            "description" : "No API key, or the key is invalid, revoked or expired (codes: missing_api_key, invalid_api_key, api_key_revoked, api_key_expired).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "403" : {
            "description" : "The key lacks the scope (insufficient_scope), or the ISP's account is suspended or terminated.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "404" : {
            "description" : "No such record in this ISP's account (not_found). Records of other ISPs also answer 404.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "429" : {
            "description" : "Rate limit reached for this key (rate_limited). Wait Retry-After seconds.",
            "headers" : {
              "Retry-After" : {
                "$ref" : "#/components/headers/Retry-After"
              }
            },
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "500" : {
            "description" : "Unexpected error on our side (internal_error).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope" : "read",
        "x-required-permission" : [ "BILLING:PLANS:VIEW" ],
        "x-changes-data" : false
      }
    },
    "/api/v1/payments/{id}" : {
      "get" : {
        "tags" : [ "Payments" ],
        "summary" : "Get a payment",
        "description" : "Returns one payment. Changes nothing.\n\nScope: `read`. The key's creator must hold BILLING:PAYMENTS:VIEW.",
        "operationId" : "getPayment",
        "parameters" : [ {
          "name" : "id",
          "in" : "path",
          "description" : "Payment id.",
          "required" : true,
          "schema" : {
            "type" : "string",
            "format" : "uuid"
          },
          "example" : "1c2d3e4f-5a6b-7c8d-9e0f-1a2b3c4d5e6f"
        }, {
          "$ref" : "#/components/parameters/TenantCode"
        } ],
        "responses" : {
          "200" : {
            "description" : "Success.",
            "headers" : {
              "X-RateLimit-Limit" : {
                "$ref" : "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining" : {
                "$ref" : "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content" : {
              "*/*" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Payment"
                }
              }
            }
          },
          "400" : {
            "description" : "The request is malformed or a parameter is invalid (codes: invalid_request, validation_failed, malformed_body, missing_parameter, invalid_parameter, tenant_required).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "401" : {
            "description" : "No API key, or the key is invalid, revoked or expired (codes: missing_api_key, invalid_api_key, api_key_revoked, api_key_expired).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "403" : {
            "description" : "The key lacks the scope (insufficient_scope), or the ISP's account is suspended or terminated.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "404" : {
            "description" : "No such record in this ISP's account (not_found). Records of other ISPs also answer 404.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "429" : {
            "description" : "Rate limit reached for this key (rate_limited). Wait Retry-After seconds.",
            "headers" : {
              "Retry-After" : {
                "$ref" : "#/components/headers/Retry-After"
              }
            },
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "500" : {
            "description" : "Unexpected error on our side (internal_error).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope" : "read",
        "x-required-permission" : [ "BILLING:PAYMENTS:VIEW" ],
        "x-changes-data" : false
      }
    },
    "/api/v1/migrations/{id}" : {
      "get" : {
        "tags" : [ "Migrations" ],
        "summary" : "Get a migration and its plan",
        "description" : "Returns one migration run with the plan it was analysed into. Changes nothing.\n\nPlatform keys only. Scope: `read`. The key's creator must hold SYSTEM:TENANTS:READ or SYSTEM:PLATFORM:MANAGE.",
        "operationId" : "getMigration",
        "parameters" : [ {
          "name" : "id",
          "in" : "path",
          "description" : "Migration id.",
          "required" : true,
          "schema" : {
            "type" : "string"
          },
          "example" : "1a85f5e9-9c8a-4e7a-8a40-a1cd9a1c2706"
        }, {
          "$ref" : "#/components/parameters/TenantCode"
        } ],
        "responses" : {
          "200" : {
            "description" : "Success.",
            "headers" : {
              "X-RateLimit-Limit" : {
                "$ref" : "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining" : {
                "$ref" : "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content" : {
              "*/*" : {
                "schema" : {
                  "$ref" : "#/components/schemas/MigrationPlan"
                }
              }
            }
          },
          "400" : {
            "description" : "The request is malformed or a parameter is invalid (codes: invalid_request, validation_failed, malformed_body, missing_parameter, invalid_parameter, tenant_required).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "401" : {
            "description" : "No API key, or the key is invalid, revoked or expired (codes: missing_api_key, invalid_api_key, api_key_revoked, api_key_expired).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "403" : {
            "description" : "The key lacks the scope (insufficient_scope), or the ISP's account is suspended or terminated.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "404" : {
            "description" : "No such record in this ISP's account (not_found). Records of other ISPs also answer 404.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "429" : {
            "description" : "Rate limit reached for this key (rate_limited). Wait Retry-After seconds.",
            "headers" : {
              "Retry-After" : {
                "$ref" : "#/components/headers/Retry-After"
              }
            },
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "500" : {
            "description" : "Unexpected error on our side (internal_error).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-key-kind" : "PLATFORM",
        "x-required-scope" : "read",
        "x-required-permission" : [ "SYSTEM:TENANTS:READ", "SYSTEM:PLATFORM:MANAGE" ],
        "x-changes-data" : false
      },
      "delete" : {
        "tags" : [ "Migrations" ],
        "summary" : "Delete a migration record",
        "description" : "Deletes the migration run and its stored export. Zones, plans and subscribers it created are NOT removed.\n\nPlatform keys only. Scope: `migrations:write`. The key's creator must hold SYSTEM:PLATFORM:MANAGE.",
        "operationId" : "deleteMigration",
        "parameters" : [ {
          "name" : "id",
          "in" : "path",
          "description" : "Migration id.",
          "required" : true,
          "schema" : {
            "type" : "string"
          },
          "example" : "1a85f5e9-9c8a-4e7a-8a40-a1cd9a1c2706"
        }, {
          "$ref" : "#/components/parameters/TenantCode"
        } ],
        "responses" : {
          "204" : {
            "description" : "Deleted.",
            "headers" : {
              "X-RateLimit-Limit" : {
                "$ref" : "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining" : {
                "$ref" : "#/components/headers/X-RateLimit-Remaining"
              }
            }
          },
          "400" : {
            "description" : "The request is malformed or a parameter is invalid (codes: invalid_request, validation_failed, malformed_body, missing_parameter, invalid_parameter, idempotency_key_required, tenant_required).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "401" : {
            "description" : "No API key, or the key is invalid, revoked or expired (codes: missing_api_key, invalid_api_key, api_key_revoked, api_key_expired).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "403" : {
            "description" : "The key lacks the scope (insufficient_scope), or the ISP's account is suspended or terminated.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "404" : {
            "description" : "No such record in this ISP's account (not_found). Records of other ISPs also answer 404.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "409" : {
            "description" : "Conflicts with the current state, e.g. idempotency_in_progress or duplicate_reference.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "422" : {
            "description" : "Refused by a business rule (e.g. invalid_state, same_plan, idempotency_key_reused); detail says why. Nothing was changed.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "429" : {
            "description" : "Rate limit reached for this key (rate_limited). Wait Retry-After seconds.",
            "headers" : {
              "Retry-After" : {
                "$ref" : "#/components/headers/Retry-After"
              }
            },
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "500" : {
            "description" : "Unexpected error on our side (internal_error).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-key-kind" : "PLATFORM",
        "x-required-scope" : "migrations:write",
        "x-required-permission" : [ "SYSTEM:PLATFORM:MANAGE" ],
        "x-changes-data" : true
      }
    },
    "/api/v1/migrations/{id}/progress" : {
      "get" : {
        "tags" : [ "Migrations" ],
        "summary" : "See how far an import has got",
        "description" : "Rows processed, created and failed so far, and why rows failed. Poll every few seconds until done, then call verifyMigration. Changes nothing.\n\nPlatform keys only. Scope: `read`. The key's creator must hold SYSTEM:TENANTS:READ or SYSTEM:PLATFORM:MANAGE.",
        "operationId" : "getMigrationProgress",
        "parameters" : [ {
          "name" : "id",
          "in" : "path",
          "description" : "Migration id.",
          "required" : true,
          "schema" : {
            "type" : "string"
          },
          "example" : "1a85f5e9-9c8a-4e7a-8a40-a1cd9a1c2706"
        }, {
          "$ref" : "#/components/parameters/TenantCode"
        } ],
        "responses" : {
          "200" : {
            "description" : "Success.",
            "headers" : {
              "X-RateLimit-Limit" : {
                "$ref" : "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining" : {
                "$ref" : "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content" : {
              "*/*" : {
                "schema" : {
                  "$ref" : "#/components/schemas/MigrationProgress"
                }
              }
            }
          },
          "400" : {
            "description" : "The request is malformed or a parameter is invalid (codes: invalid_request, validation_failed, malformed_body, missing_parameter, invalid_parameter, tenant_required).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "401" : {
            "description" : "No API key, or the key is invalid, revoked or expired (codes: missing_api_key, invalid_api_key, api_key_revoked, api_key_expired).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "403" : {
            "description" : "The key lacks the scope (insufficient_scope), or the ISP's account is suspended or terminated.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "404" : {
            "description" : "No such record in this ISP's account (not_found). Records of other ISPs also answer 404.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "429" : {
            "description" : "Rate limit reached for this key (rate_limited). Wait Retry-After seconds.",
            "headers" : {
              "Retry-After" : {
                "$ref" : "#/components/headers/Retry-After"
              }
            },
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "500" : {
            "description" : "Unexpected error on our side (internal_error).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-key-kind" : "PLATFORM",
        "x-required-scope" : "read",
        "x-required-permission" : [ "SYSTEM:TENANTS:READ", "SYSTEM:PLATFORM:MANAGE" ],
        "x-changes-data" : false
      }
    },
    "/api/v1/invoices" : {
      "get" : {
        "tags" : [ "Invoices" ],
        "summary" : "List invoices",
        "description" : "Lists the ISP's invoices, newest first, filtered by status, issue date or a search over the subscriber's name, phone, username and the invoice number. For one subscriber's invoices use listSubscriberInvoices. Changes nothing.\n\nScope: `read`. The key's creator must hold SYSTEM:DASHBOARD:VIEW.",
        "operationId" : "listInvoices",
        "parameters" : [ {
          "name" : "status",
          "in" : "query",
          "description" : "Only invoices in this state.",
          "required" : false,
          "schema" : {
            "type" : "string",
            "enum" : [ "ISSUED", "PAID", "OVERDUE", "VOID" ]
          }
        }, {
          "name" : "from",
          "in" : "query",
          "description" : "Issued on or after this date (YYYY-MM-DD).",
          "required" : false,
          "schema" : {
            "type" : "string",
            "format" : "date"
          },
          "example" : "2026-10-01"
        }, {
          "name" : "to",
          "in" : "query",
          "description" : "Issued on or before this date (YYYY-MM-DD).",
          "required" : false,
          "schema" : {
            "type" : "string",
            "format" : "date"
          },
          "example" : "2026-10-31"
        }, {
          "name" : "q",
          "in" : "query",
          "description" : "Free-text search.",
          "required" : false,
          "schema" : {
            "type" : "string"
          },
          "example" : "INV-202610"
        }, {
          "name" : "page",
          "in" : "query",
          "description" : "Page number, starting at 1.",
          "required" : false,
          "schema" : {
            "minimum" : 1,
            "type" : "integer",
            "format" : "int32",
            "default" : 1
          },
          "example" : 1
        }, {
          "name" : "size",
          "in" : "query",
          "description" : "Results per page, 1–200.",
          "required" : false,
          "schema" : {
            "maximum" : 200,
            "minimum" : 1,
            "type" : "integer",
            "format" : "int32",
            "default" : 50
          },
          "example" : 50
        }, {
          "$ref" : "#/components/parameters/TenantCode"
        } ],
        "responses" : {
          "200" : {
            "description" : "Success.",
            "headers" : {
              "X-RateLimit-Limit" : {
                "$ref" : "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining" : {
                "$ref" : "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content" : {
              "*/*" : {
                "schema" : {
                  "$ref" : "#/components/schemas/PagedResultInvoice"
                }
              }
            }
          },
          "400" : {
            "description" : "The request is malformed or a parameter is invalid (codes: invalid_request, validation_failed, malformed_body, missing_parameter, invalid_parameter, tenant_required).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "401" : {
            "description" : "No API key, or the key is invalid, revoked or expired (codes: missing_api_key, invalid_api_key, api_key_revoked, api_key_expired).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "403" : {
            "description" : "The key lacks the scope (insufficient_scope), or the ISP's account is suspended or terminated.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "429" : {
            "description" : "Rate limit reached for this key (rate_limited). Wait Retry-After seconds.",
            "headers" : {
              "Retry-After" : {
                "$ref" : "#/components/headers/Retry-After"
              }
            },
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "500" : {
            "description" : "Unexpected error on our side (internal_error).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope" : "read",
        "x-required-permission" : [ "SYSTEM:DASHBOARD:VIEW" ],
        "x-changes-data" : false
      }
    },
    "/api/v1/invoices/{id}" : {
      "get" : {
        "tags" : [ "Invoices" ],
        "summary" : "Get an invoice",
        "description" : "Returns one invoice. Changes nothing.\n\nScope: `read`. The key's creator must hold SYSTEM:DASHBOARD:VIEW.",
        "operationId" : "getInvoice",
        "parameters" : [ {
          "name" : "id",
          "in" : "path",
          "description" : "Invoice id.",
          "required" : true,
          "schema" : {
            "type" : "string",
            "format" : "uuid"
          },
          "example" : "7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d"
        }, {
          "$ref" : "#/components/parameters/TenantCode"
        } ],
        "responses" : {
          "200" : {
            "description" : "Success.",
            "headers" : {
              "X-RateLimit-Limit" : {
                "$ref" : "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining" : {
                "$ref" : "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content" : {
              "*/*" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Invoice"
                }
              }
            }
          },
          "400" : {
            "description" : "The request is malformed or a parameter is invalid (codes: invalid_request, validation_failed, malformed_body, missing_parameter, invalid_parameter, tenant_required).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "401" : {
            "description" : "No API key, or the key is invalid, revoked or expired (codes: missing_api_key, invalid_api_key, api_key_revoked, api_key_expired).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "403" : {
            "description" : "The key lacks the scope (insufficient_scope), or the ISP's account is suspended or terminated.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "404" : {
            "description" : "No such record in this ISP's account (not_found). Records of other ISPs also answer 404.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "429" : {
            "description" : "Rate limit reached for this key (rate_limited). Wait Retry-After seconds.",
            "headers" : {
              "Retry-After" : {
                "$ref" : "#/components/headers/Retry-After"
              }
            },
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "500" : {
            "description" : "Unexpected error on our side (internal_error).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope" : "read",
        "x-required-permission" : [ "SYSTEM:DASHBOARD:VIEW" ],
        "x-changes-data" : false
      }
    },
    "/api/v1/account" : {
      "get" : {
        "tags" : [ "Account" ],
        "summary" : "Get the API key and the ISP it acts on",
        "description" : "Returns the calling key (name, scopes, rate limit, expiry) and the ISP it acts on. Use it first to check a key works and to learn the ISP's currency and time zone. Changes nothing.\n\nScope: `read`.",
        "operationId" : "getAccount",
        "parameters" : [ {
          "$ref" : "#/components/parameters/TenantCode"
        } ],
        "responses" : {
          "200" : {
            "description" : "Success.",
            "headers" : {
              "X-RateLimit-Limit" : {
                "$ref" : "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining" : {
                "$ref" : "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content" : {
              "*/*" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Account"
                }
              }
            }
          },
          "400" : {
            "description" : "The request is malformed or a parameter is invalid (codes: invalid_request, validation_failed, malformed_body, missing_parameter, invalid_parameter, tenant_required).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "401" : {
            "description" : "No API key, or the key is invalid, revoked or expired (codes: missing_api_key, invalid_api_key, api_key_revoked, api_key_expired).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "403" : {
            "description" : "The key lacks the scope (insufficient_scope), or the ISP's account is suspended or terminated.",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "429" : {
            "description" : "Rate limit reached for this key (rate_limited). Wait Retry-After seconds.",
            "headers" : {
              "Retry-After" : {
                "$ref" : "#/components/headers/Retry-After"
              }
            },
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          },
          "500" : {
            "description" : "Unexpected error on our side (internal_error).",
            "content" : {
              "application/problem+json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "x-required-scope" : "read",
        "x-changes-data" : false
      }
    }
  },
  "components" : {
    "schemas" : {
      "WebhookEndpoint" : {
        "type" : "object",
        "properties" : {
          "id" : {
            "type" : "string",
            "description" : "Endpoint id.",
            "example" : "2b3c4d5e-6f7a-8b9c-0d1e-2f3a4b5c6d7e"
          },
          "url" : {
            "type" : "string",
            "description" : "HTTPS URL events are POSTed to.",
            "example" : "https://app.getcrewbot.com/hooks/coresync"
          },
          "description" : {
            "type" : "string",
            "description" : "A note to say what the endpoint is for.",
            "example" : "crew.bot"
          },
          "events" : {
            "type" : "array",
            "description" : "Events this endpoint receives.",
            "example" : [ "payment.received", "subscriber.suspended" ],
            "items" : {
              "type" : "string",
              "description" : "Events this endpoint receives.",
              "example" : "[\"payment.received\",\"subscriber.suspended\"]"
            }
          },
          "active" : {
            "type" : "boolean",
            "description" : "False when switched off by hand, or automatically after 20 failed attempts in a row.",
            "example" : true
          },
          "disabled_reason" : {
            "type" : "string",
            "description" : "Why it was switched off automatically, if it was."
          },
          "last_success_at" : {
            "type" : "string",
            "description" : "Last successful delivery (UTC).",
            "format" : "date-time",
            "example" : "2026-10-25T09:00:06Z"
          },
          "last_failure_at" : {
            "type" : "string",
            "description" : "Last failed attempt (UTC).",
            "format" : "date-time"
          },
          "created_at" : {
            "type" : "string",
            "description" : "When the endpoint was added (UTC).",
            "format" : "date-time",
            "example" : "2026-10-25T08:00:00Z"
          },
          "signing_secret" : {
            "type" : "string",
            "description" : "The secret to verify X-Webhook-Signature with. Present only in the response to createWebhookEndpoint; store it then.",
            "example" : "whsec_6Jx0..."
          }
        },
        "description" : "A URL that receives events. The signing secret is only returned when the endpoint is created."
      },
      "CreateWebhookEndpointRequest" : {
        "required" : [ "events", "url" ],
        "type" : "object",
        "properties" : {
          "url" : {
            "maxLength" : 500,
            "minLength" : 0,
            "type" : "string",
            "description" : "HTTPS URL to POST events to. Must be reachable from the internet.",
            "example" : "https://app.getcrewbot.com/hooks/coresync"
          },
          "events" : {
            "type" : "array",
            "description" : "Events to receive.",
            "items" : {
              "type" : "string",
              "enum" : [ "payment.received", "subscriber.suspended", "subscriber.reactivated", "session.down" ]
            }
          },
          "description" : {
            "maxLength" : 200,
            "minLength" : 0,
            "type" : "string",
            "description" : "A note to say what it is for.",
            "example" : "crew.bot"
          }
        },
        "description" : "A new webhook endpoint."
      },
      "WebhookDelivery" : {
        "type" : "object",
        "properties" : {
          "id" : {
            "type" : "string",
            "description" : "Delivery id; also sent as X-Webhook-Delivery-Id.",
            "example" : "8d9e0f1a-2b3c-4d5e-6f7a-8b9c0d1e2f3a"
          },
          "endpoint_id" : {
            "type" : "string",
            "description" : "The endpoint.",
            "example" : "2b3c4d5e-6f7a-8b9c-0d1e-2f3a4b5c6d7e"
          },
          "event_id" : {
            "type" : "string",
            "description" : "The event's id (the envelope's id). The same event sent to two endpoints has one event_id.",
            "example" : "evt_1f2e3d4c5b6a"
          },
          "event_type" : {
            "type" : "string",
            "description" : "Event type.",
            "example" : "payment.received"
          },
          "status" : {
            "type" : "string",
            "description" : "PENDING: waiting for its next attempt. DELIVERED: the endpoint answered 2xx. FAILED: gave up after 5 attempts.",
            "example" : "DELIVERED",
            "enum" : [ "PENDING", "DELIVERED", "FAILED" ]
          },
          "attempts" : {
            "type" : "integer",
            "description" : "Attempts made so far (at most 5).",
            "format" : "int32",
            "example" : 1
          },
          "response_status" : {
            "type" : "integer",
            "description" : "HTTP status of the last attempt, if the endpoint answered.",
            "format" : "int32",
            "example" : 200
          },
          "last_error" : {
            "type" : "string",
            "description" : "What went wrong on the last attempt, if it failed."
          },
          "next_attempt_at" : {
            "type" : "string",
            "description" : "When the next retry happens (UTC), while PENDING.",
            "format" : "date-time"
          },
          "created_at" : {
            "type" : "string",
            "description" : "When the event was queued (UTC).",
            "format" : "date-time",
            "example" : "2026-10-25T09:00:05Z"
          },
          "delivered_at" : {
            "type" : "string",
            "description" : "When the endpoint accepted it (UTC).",
            "format" : "date-time",
            "example" : "2026-10-25T09:00:06Z"
          }
        },
        "description" : "One event sent (or being sent) to one endpoint."
      },
      "Tenant" : {
        "type" : "object",
        "properties" : {
          "code" : {
            "type" : "string",
            "description" : "The ISP's code. A platform key passes it in X-Tenant-Code.",
            "example" : "EIKAL"
          },
          "name" : {
            "type" : "string",
            "description" : "Company name.",
            "example" : "Eikal Networks"
          },
          "status" : {
            "type" : "string",
            "description" : "Account status.",
            "example" : "ACTIVE",
            "enum" : [ "PROVISIONING", "TRIAL", "ACTIVE", "SUSPENDED", "CANCELLED", "FAILED", "TERMINATED" ]
          },
          "currency" : {
            "type" : "string",
            "description" : "ISO 4217 currency the ISP bills in.",
            "example" : "KES"
          },
          "country_code" : {
            "type" : "string",
            "description" : "ISO 3166-1 alpha-2 country.",
            "example" : "KE"
          },
          "timezone" : {
            "type" : "string",
            "description" : "IANA time zone the ISP works in.",
            "example" : "Africa/Nairobi"
          }
        },
        "description" : "An ISP on CoreSync."
      },
      "TenantCreated" : {
        "type" : "object",
        "properties" : {
          "tenant" : {
            "$ref" : "#/components/schemas/Tenant"
          },
          "portal_url" : {
            "type" : "string",
            "description" : "Where the ISP's staff sign in.",
            "example" : "https://skynet.coresyncisp.com/admin/login"
          },
          "admin_email" : {
            "type" : "string",
            "description" : "The first admin's login email.",
            "example" : "ops@skynet.co.ke"
          },
          "welcome_email" : {
            "type" : "string",
            "description" : "sent: the login went out by email. deferred: call sendTenantWelcomeEmail when ready.",
            "example" : "sent",
            "enum" : [ "sent", "deferred" ]
          }
        },
        "description" : "A newly provisioned ISP."
      },
      "CreateTenantRequest" : {
        "required" : [ "code", "company_name", "contact_person", "email", "phone" ],
        "type" : "object",
        "properties" : {
          "code" : {
            "pattern" : "^[a-z0-9_-]{2,30}$",
            "type" : "string",
            "description" : "Short code for the ISP, used in its portal address (code.coresyncisp.com) and as X-Tenant-Code. Lowercase here; stored and returned in upper case.",
            "example" : "skynet"
          },
          "company_name" : {
            "maxLength" : 200,
            "minLength" : 0,
            "type" : "string",
            "description" : "The ISP's company name.",
            "example" : "Skynet Communications Ltd"
          },
          "email" : {
            "type" : "string",
            "description" : "Email of the ISP's first admin; their login and the welcome email go here.",
            "example" : "ops@skynet.co.ke"
          },
          "contact_person" : {
            "maxLength" : 200,
            "minLength" : 0,
            "type" : "string",
            "description" : "The admin's full name.",
            "example" : "Mary Achieng"
          },
          "phone" : {
            "type" : "string",
            "description" : "Contact phone, any common format; stored as given.",
            "example" : "+254711000222"
          },
          "address" : {
            "maxLength" : 500,
            "minLength" : 0,
            "type" : "string",
            "description" : "Physical address.",
            "example" : "Kisumu"
          },
          "max_subscribers" : {
            "minimum" : 1,
            "type" : "integer",
            "description" : "Subscriber limit; the platform default when omitted.",
            "format" : "int32",
            "example" : 2000
          },
          "max_nas_devices" : {
            "minimum" : 1,
            "type" : "integer",
            "description" : "Router (NAS) limit; the platform default when omitted.",
            "format" : "int32",
            "example" : 10
          },
          "migration" : {
            "type" : "boolean",
            "description" : "True when the ISP is coming off another billing system (turns on the migration onboarding steps).",
            "example" : true
          },
          "previous_system" : {
            "maxLength" : 100,
            "minLength" : 0,
            "type" : "string",
            "description" : "The system they are leaving, when migration is true.",
            "example" : "Vinea"
          },
          "defer_welcome_email" : {
            "type" : "boolean",
            "description" : "true holds the welcome email (with the login) until sendTenantWelcomeEmail is called; default false sends it now.",
            "example" : false
          }
        },
        "description" : "A new ISP account. The admin user's password is generated by CoreSync and only ever sent to them by email."
      },
      "WelcomeEmailResult" : {
        "type" : "object",
        "properties" : {
          "sent_to" : {
            "type" : "string",
            "description" : "The admin it was sent to.",
            "example" : "ops@skynet.co.ke"
          },
          "password_reset" : {
            "type" : "boolean",
            "description" : "Always true: a fresh password is issued and emailed; the old one stops working.",
            "example" : true
          }
        },
        "description" : "A welcome email that went out."
      },
      "Balance" : {
        "type" : "object",
        "properties" : {
          "credit" : {
            "$ref" : "#/components/schemas/Money"
          },
          "outstanding" : {
            "$ref" : "#/components/schemas/Money"
          },
          "open_invoices" : {
            "type" : "integer",
            "description" : "How many invoices are unpaid.",
            "format" : "int32",
            "example" : 1
          },
          "next_renewal_amount" : {
            "$ref" : "#/components/schemas/Money"
          },
          "expires_at" : {
            "type" : "string",
            "description" : "When the current subscription runs out (UTC).",
            "format" : "date-time",
            "example" : "2026-11-25T09:00:00Z"
          }
        },
        "description" : "What a subscriber has paid ahead and what they owe."
      },
      "Money" : {
        "type" : "object",
        "properties" : {
          "amount" : {
            "pattern" : "^-?\\d+\\.\\d{2}$",
            "type" : "string",
            "description" : "Decimal amount as a string.",
            "example" : "1500.00"
          },
          "currency" : {
            "maxLength" : 3,
            "minLength" : 3,
            "type" : "string",
            "description" : "ISO 4217 currency code.",
            "example" : "KES"
          }
        },
        "description" : "An amount of money. The amount is a decimal string with two decimals, so it never loses precision."
      },
      "PlanSummary" : {
        "type" : "object",
        "properties" : {
          "id" : {
            "type" : "string",
            "description" : "Plan id. Pass it to changeSubscriberPlan.",
            "example" : "9c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f"
          },
          "code" : {
            "type" : "string",
            "description" : "Plan code.",
            "example" : "HOME10"
          },
          "name" : {
            "type" : "string",
            "description" : "Plan name as customers see it.",
            "example" : "Home 10 Mbps"
          },
          "price" : {
            "$ref" : "#/components/schemas/Money"
          }
        },
        "description" : "A short reference to a plan."
      },
      "Subscriber" : {
        "type" : "object",
        "properties" : {
          "id" : {
            "type" : "string",
            "description" : "Subscriber id.",
            "example" : "3f2a6c1e-8b7d-4c1a-9e2f-1a2b3c4d5e6f"
          },
          "account_number" : {
            "type" : "string",
            "description" : "The ISP's account number.",
            "example" : "ACC-00123"
          },
          "subscriber_code" : {
            "type" : "string",
            "description" : "The billing code the customer types as the M-Pesa paybill account reference.",
            "example" : "EK1234"
          },
          "username" : {
            "type" : "string",
            "description" : "PPPoE / RADIUS login name.",
            "example" : "jane.w"
          },
          "name" : {
            "type" : "string",
            "description" : "Full name, or the business name for a business account.",
            "example" : "Jane Wanjiru"
          },
          "first_name" : {
            "type" : "string",
            "description" : "First name.",
            "example" : "Jane"
          },
          "last_name" : {
            "type" : "string",
            "description" : "Last name.",
            "example" : "Wanjiru"
          },
          "phone" : {
            "type" : "string",
            "description" : "Primary phone number in E.164.",
            "example" : "+254712345678"
          },
          "phones" : {
            "type" : "array",
            "description" : "Every phone number on the account, in E.164 (some accounts carry two).",
            "example" : [ "+254712345678" ],
            "items" : {
              "type" : "string",
              "description" : "Every phone number on the account, in E.164 (some accounts carry two).",
              "example" : "[\"+254712345678\"]"
            }
          },
          "email" : {
            "type" : "string",
            "description" : "Email address, if the ISP has one.",
            "example" : "jane@example.com"
          },
          "type" : {
            "type" : "string",
            "description" : "Account type.",
            "example" : "RESIDENTIAL",
            "enum" : [ "RESIDENTIAL", "BUSINESS", "CORPORATE" ]
          },
          "status" : {
            "type" : "string",
            "description" : "Account status. ACTIVE: connected and billed. SUSPENDED: service off (unpaid, expired or by hand). PENDING / DRAFT: being set up. TERMINATED: closed.",
            "example" : "ACTIVE",
            "enum" : [ "DRAFT", "PENDING", "ACTIVE", "SUSPENDED", "TERMINATED" ]
          },
          "service_type" : {
            "type" : "string",
            "description" : "How the subscriber connects.",
            "example" : "PPPOE",
            "enum" : [ "PPPOE", "HOTSPOT", "STATIC", "BOTH" ]
          },
          "address" : {
            "type" : "string",
            "description" : "Installation or physical address.",
            "example" : "Kilimani, Nairobi"
          },
          "zone" : {
            "type" : "string",
            "description" : "Service zone or area name.",
            "example" : "Kilimani"
          },
          "online" : {
            "type" : "boolean",
            "description" : "True while the subscriber has a live session.",
            "example" : true
          },
          "last_online_at" : {
            "type" : "string",
            "description" : "When the subscriber was last seen online (UTC).",
            "format" : "date-time",
            "example" : "2026-10-25T08:55:00Z"
          },
          "subscription" : {
            "$ref" : "#/components/schemas/Subscription"
          },
          "balance" : {
            "$ref" : "#/components/schemas/Balance"
          },
          "created_at" : {
            "type" : "string",
            "description" : "When the account was created (UTC).",
            "format" : "date-time",
            "example" : "2025-03-02T10:15:00Z"
          },
          "activated_at" : {
            "type" : "string",
            "description" : "When service was last activated (UTC).",
            "format" : "date-time",
            "example" : "2025-03-02T12:00:00Z"
          },
          "suspended_at" : {
            "type" : "string",
            "description" : "When service was last suspended (UTC), if it is suspended.",
            "format" : "date-time"
          }
        },
        "description" : "A subscriber (an ISP's customer) with their current subscription and balance."
      },
      "SubscriberChange" : {
        "type" : "object",
        "properties" : {
          "outcome" : {
            "type" : "string",
            "description" : "done: the change was made. approval_pending: the ISP requires a manager's approval for this action; a request was filed and the change happens when someone approves it in the admin.",
            "example" : "done",
            "enum" : [ "done", "approval_pending" ]
          },
          "subscriber" : {
            "$ref" : "#/components/schemas/Subscriber"
          },
          "approval_request_id" : {
            "type" : "string",
            "description" : "The approval request filed, when outcome is approval_pending."
          }
        },
        "description" : "The result of a change to a subscriber."
      },
      "Subscription" : {
        "type" : "object",
        "properties" : {
          "id" : {
            "type" : "string",
            "description" : "Subscription id.",
            "example" : "5b6c7d8e-9f0a-1b2c-3d4e-5f6a7b8c9d0e"
          },
          "status" : {
            "type" : "string",
            "description" : "Subscription status. EXPIRED means the end date passed without a renewal.",
            "example" : "ACTIVE",
            "enum" : [ "PENDING", "ACTIVE", "EXPIRED", "SUSPENDED", "CANCELLED" ]
          },
          "plan" : {
            "$ref" : "#/components/schemas/PlanSummary"
          },
          "starts_at" : {
            "type" : "string",
            "description" : "Start of the current cycle (UTC).",
            "format" : "date-time",
            "example" : "2026-10-25T09:00:00Z"
          },
          "expires_at" : {
            "type" : "string",
            "description" : "End of the current cycle (UTC). Service stops after this unless renewed.",
            "format" : "date-time",
            "example" : "2026-11-25T09:00:00Z"
          },
          "auto_renew" : {
            "type" : "boolean",
            "description" : "Whether the next cycle is taken from wallet credit automatically.",
            "example" : true
          },
          "price" : {
            "$ref" : "#/components/schemas/Money"
          },
          "pending_plan_id" : {
            "type" : "string",
            "description" : "A plan change scheduled for the next renewal, if any."
          }
        },
        "description" : "The subscriber's current (or latest) subscription: which plan, and until when."
      },
      "SuspendSubscriberRequest" : {
        "required" : [ "reason" ],
        "type" : "object",
        "properties" : {
          "reason" : {
            "maxLength" : 300,
            "minLength" : 3,
            "type" : "string",
            "description" : "Reason, shown on the subscriber's timeline and to approvers.",
            "example" : "Customer asked to pause service while travelling"
          }
        },
        "description" : "Why the subscriber is being suspended. Recorded on their timeline."
      },
      "ReactivateSubscriberRequest" : {
        "required" : [ "reason" ],
        "type" : "object",
        "properties" : {
          "reason" : {
            "maxLength" : 300,
            "minLength" : 3,
            "type" : "string",
            "description" : "Reason, shown on the subscriber's timeline.",
            "example" : "Paid in cash at the office; receipt to follow"
          }
        },
        "description" : "Why the subscriber is being reactivated without a payment. To reactivate by payment, use recordPayment instead."
      },
      "ChangePlanRequest" : {
        "required" : [ "plan_id", "reason" ],
        "type" : "object",
        "properties" : {
          "plan_id" : {
            "type" : "string",
            "description" : "The new plan (an ACTIVE plan from listPlans).",
            "example" : "9c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f"
          },
          "reason" : {
            "pattern" : "^(CUSTOMER_REQUEST|AFFORDABILITY|PROMOTION|RETENTION_OFFER|SERVICE_QUALITY|BILLING_CORRECTION|MIGRATION|OTHER)$",
            "type" : "string",
            "description" : "Why the plan is changing; reported in the ISP's plan-change reports.",
            "example" : "CUSTOMER_REQUEST",
            "enum" : [ "CUSTOMER_REQUEST", "AFFORDABILITY", "PROMOTION", "RETENTION_OFFER", "SERVICE_QUALITY", "BILLING_CORRECTION", "MIGRATION", "OTHER" ]
          },
          "notes" : {
            "maxLength" : 300,
            "minLength" : 0,
            "type" : "string",
            "description" : "Notes. Required when reason is OTHER.",
            "example" : "Moving to the business plan after opening a shop"
          }
        },
        "description" : "Move a subscriber to another plan now, without a payment and without moving their expiry date."
      },
      "Payment" : {
        "type" : "object",
        "properties" : {
          "id" : {
            "type" : "string",
            "description" : "Payment id.",
            "example" : "1c2d3e4f-5a6b-7c8d-9e0f-1a2b3c4d5e6f"
          },
          "subscriber" : {
            "$ref" : "#/components/schemas/SubscriberRef"
          },
          "amount" : {
            "$ref" : "#/components/schemas/Money"
          },
          "provider" : {
            "type" : "string",
            "description" : "Where the money came from, e.g. MPESA, KCB, EQUITY, CASH, BANK.",
            "example" : "MPESA"
          },
          "method" : {
            "type" : "string",
            "description" : "How it arrived: STK_PUSH, PAYBILL, TILL, MANUAL (recorded by staff or the API).",
            "example" : "PAYBILL"
          },
          "reference" : {
            "type" : "string",
            "description" : "The provider's receipt / transaction reference. Unique per provider.",
            "example" : "SJK3H7Y2QX"
          },
          "payer_phone" : {
            "type" : "string",
            "description" : "The payer's phone in E.164, when the provider gives it.",
            "example" : "+254712345678"
          },
          "account_reference" : {
            "type" : "string",
            "description" : "What the payer typed as the account / reference.",
            "example" : "EK1234"
          },
          "status" : {
            "type" : "string",
            "description" : "PENDING, COMPLETED, FAILED or REVERSED.",
            "example" : "COMPLETED",
            "enum" : [ "PENDING", "COMPLETED", "FAILED", "REVERSED" ]
          },
          "applied" : {
            "type" : "boolean",
            "description" : "True once the money has been credited to the subscriber's wallet / renewal.",
            "example" : true
          },
          "applied_at" : {
            "type" : "string",
            "description" : "When it was applied (UTC).",
            "format" : "date-time",
            "example" : "2026-10-25T09:00:05Z"
          },
          "not_applied_amount" : {
            "$ref" : "#/components/schemas/Money"
          },
          "received_at" : {
            "type" : "string",
            "description" : "When the payment was received (UTC).",
            "format" : "date-time",
            "example" : "2026-10-25T09:00:00Z"
          }
        },
        "description" : "Money received from a customer: M-Pesa, bank, cash or recorded through the API."
      },
      "SubscriberRef" : {
        "type" : "object",
        "properties" : {
          "id" : {
            "type" : "string",
            "description" : "Subscriber id.",
            "example" : "3f2a6c1e-8b7d-4c1a-9e2f-1a2b3c4d5e6f"
          },
          "account_number" : {
            "type" : "string",
            "description" : "The ISP's account number for this subscriber.",
            "example" : "ACC-00123"
          },
          "name" : {
            "type" : "string",
            "description" : "Full name, or the business name for a business account.",
            "example" : "Jane Wanjiru"
          },
          "phone" : {
            "type" : "string",
            "description" : "Primary phone number in E.164.",
            "example" : "+254712345678"
          }
        },
        "description" : "A short reference to a subscriber."
      },
      "RecordPaymentRequest" : {
        "required" : [ "amount", "subscriber_id" ],
        "type" : "object",
        "properties" : {
          "subscriber_id" : {
            "type" : "string",
            "description" : "The subscriber who paid.",
            "example" : "3f2a6c1e-8b7d-4c1a-9e2f-1a2b3c4d5e6f"
          },
          "amount" : {
            "pattern" : "^\\d{1,9}(\\.\\d{1,2})?$",
            "type" : "string",
            "description" : "Amount received, as a decimal string.",
            "example" : "1500.00"
          },
          "currency" : {
            "maxLength" : 3,
            "minLength" : 3,
            "type" : "string",
            "description" : "ISO 4217 currency. Must be the ISP's currency; defaults to it.",
            "example" : "KES"
          },
          "provider" : {
            "pattern" : "^(CASH|MPESA|BANK|CHEQUE|CARD|OTHER)$",
            "type" : "string",
            "description" : "Where the money came from. Defaults to CASH.",
            "example" : "MPESA",
            "enum" : [ "CASH", "MPESA", "BANK", "CHEQUE", "CARD", "OTHER" ]
          },
          "reference" : {
            "maxLength" : 100,
            "minLength" : 0,
            "type" : "string",
            "description" : "The receipt or transaction reference. A provider + reference pair is only ever recorded once, so pass the real receipt (e.g. the M-Pesa code) to make a duplicate impossible. Generated from the Idempotency-Key when omitted.",
            "example" : "SJK3H7Y2QX"
          }
        },
        "description" : "A payment received outside CoreSync's own channels (cash, a bank transfer, an M-Pesa receipt the ISP holds)."
      },
      "Migration" : {
        "type" : "object",
        "properties" : {
          "id" : {
            "type" : "string",
            "description" : "Run id.",
            "example" : "1a85f5e9-9c8a-4e7a-8a40-a1cd9a1c2706"
          },
          "status" : {
            "type" : "string",
            "description" : "ANALYSED: read, nothing created. APPLIED: zones, plans and speed profiles created; subscribers not yet. IMPORTING / IMPORTED: subscribers being / been created. VERIFIED: checked against the export. FAILED.",
            "example" : "APPLIED",
            "enum" : [ "ANALYSED", "APPLIED", "IMPORTING", "IMPORTED", "VERIFIED", "FAILED" ]
          },
          "source" : {
            "type" : "string",
            "description" : "The old system detected.",
            "example" : "Vinea"
          },
          "file_name" : {
            "type" : "string",
            "description" : "The uploaded file.",
            "example" : "vinea-customers.csv"
          },
          "row_count" : {
            "type" : "integer",
            "description" : "Rows read from the export.",
            "format" : "int32",
            "example" : 1240
          },
          "rows_ready" : {
            "type" : "integer",
            "description" : "Rows that will be imported as subscribers.",
            "format" : "int32",
            "example" : 1198
          },
          "rows_excluded" : {
            "type" : "integer",
            "description" : "Rows left out (e.g. terminated accounts, unless included).",
            "format" : "int32",
            "example" : 42
          },
          "imported_success" : {
            "type" : "integer",
            "description" : "Subscribers created by the import.",
            "format" : "int32",
            "example" : 1198
          },
          "imported_failed" : {
            "type" : "integer",
            "description" : "Rows the import could not create.",
            "format" : "int32",
            "example" : 0
          },
          "created_by" : {
            "type" : "string",
            "description" : "Who started it.",
            "example" : "crew.bot (Avitech)"
          },
          "created_at" : {
            "type" : "string",
            "description" : "When it was uploaded (UTC).",
            "format" : "date-time",
            "example" : "2026-10-25T09:00:00Z"
          },
          "applied_at" : {
            "type" : "string",
            "description" : "When it was applied (UTC).",
            "format" : "date-time",
            "example" : "2026-10-25T09:10:00Z"
          },
          "verified_at" : {
            "type" : "string",
            "description" : "When it was last verified (UTC).",
            "format" : "date-time",
            "example" : "2026-10-25T10:00:00Z"
          },
          "error" : {
            "type" : "string",
            "description" : "What went wrong, when status is FAILED.",
            "example" : "Missing price for package 20Mbps"
          }
        },
        "description" : "One migration run: an uploaded export moving through analyse → apply → import → verify."
      },
      "MigrationNas" : {
        "type" : "object",
        "properties" : {
          "key" : {
            "type" : "string",
            "description" : "Key to use in applyMigration's nas_devices.",
            "example" : "kilimani-ccr"
          },
          "name" : {
            "type" : "string",
            "description" : "Name in the export.",
            "example" : "Kilimani CCR"
          },
          "subscriber_count" : {
            "type" : "integer",
            "description" : "Customers on it.",
            "format" : "int32",
            "example" : 212
          },
          "matched_nas_id" : {
            "type" : "string",
            "description" : "The ISP's router it was matched to automatically, if any."
          }
        },
        "description" : "A router named in the export."
      },
      "MigrationPackage" : {
        "type" : "object",
        "properties" : {
          "key" : {
            "type" : "string",
            "description" : "Key to use in applyMigration's packages.",
            "example" : "10mbps"
          },
          "label" : {
            "type" : "string",
            "description" : "The old system's label.",
            "example" : "10Mbps Home"
          },
          "subscriber_count" : {
            "type" : "integer",
            "description" : "Customers on it.",
            "format" : "int32",
            "example" : 640
          },
          "download_mbps" : {
            "type" : "integer",
            "description" : "Download speed read from the export.",
            "format" : "int32",
            "example" : 10
          },
          "upload_mbps" : {
            "type" : "integer",
            "description" : "Upload speed read from the export.",
            "format" : "int32",
            "example" : 10
          },
          "existing_plan" : {
            "type" : "string",
            "description" : "An existing plan that will be reused, if any.",
            "example" : "Home 10 Mbps"
          },
          "suggested_plan_code" : {
            "type" : "string",
            "description" : "Plan code that will be used.",
            "example" : "HOME10"
          },
          "suggested_price" : {
            "$ref" : "#/components/schemas/Money"
          },
          "price_required" : {
            "type" : "boolean",
            "description" : "True when there is no price to suggest; applyMigration must supply one.",
            "example" : false
          },
          "price_uncertain" : {
            "type" : "boolean",
            "description" : "True when payments disagree enough that the suggested price should be checked.",
            "example" : false
          }
        },
        "description" : "A package (product) in the export."
      },
      "MigrationPlan" : {
        "type" : "object",
        "properties" : {
          "migration" : {
            "$ref" : "#/components/schemas/Migration"
          },
          "source_key" : {
            "type" : "string",
            "description" : "The old system detected (pass as source to override).",
            "example" : "VINEA"
          },
          "confidence" : {
            "type" : "number",
            "description" : "How sure the detection is, 0–1.",
            "format" : "double",
            "example" : 1.0
          },
          "available_sources" : {
            "type" : "array",
            "description" : "Every old system CoreSync can read, by key.",
            "items" : {
              "type" : "string",
              "description" : "Every old system CoreSync can read, by key."
            }
          },
          "total_rows" : {
            "type" : "integer",
            "description" : "Rows read.",
            "format" : "int32",
            "example" : 1240
          },
          "headers" : {
            "type" : "array",
            "description" : "Column headers found in the file.",
            "items" : {
              "type" : "string",
              "description" : "Column headers found in the file."
            }
          },
          "status_counts" : {
            "type" : "object",
            "additionalProperties" : {
              "type" : "integer",
              "description" : "Customers per status, as CoreSync will import them (ACTIVE, SUSPENDED, TERMINATED…).",
              "format" : "int32"
            },
            "description" : "Customers per status, as CoreSync will import them (ACTIVE, SUSPENDED, TERMINATED…)."
          },
          "suggested_node_strategy" : {
            "type" : "string",
            "description" : "How the export's nodes will be organised: NODES_AS_ZONES (each node becomes a zone) or NODES_AS_BUILDINGS (nodes become buildings inside the export's zones).",
            "example" : "NODES_AS_ZONES",
            "enum" : [ "NODES_AS_ZONES", "NODES_AS_BUILDINGS" ]
          },
          "zones" : {
            "type" : "array",
            "description" : "Areas found. Each becomes a zone, or reuses the zone with the same name.",
            "items" : {
              "$ref" : "#/components/schemas/MigrationZone"
            }
          },
          "packages" : {
            "type" : "array",
            "description" : "Packages found. Each becomes a plan (and a speed profile), or reuses a matching one. A package with price_required true needs a price in applyMigration.",
            "items" : {
              "$ref" : "#/components/schemas/MigrationPackage"
            }
          },
          "nas_devices" : {
            "type" : "array",
            "description" : "Routers named in the export; map each to one of the ISP's routers in applyMigration.",
            "items" : {
              "$ref" : "#/components/schemas/MigrationNas"
            }
          },
          "warnings" : {
            "type" : "array",
            "description" : "Things to check before applying.",
            "items" : {
              "type" : "string",
              "description" : "Things to check before applying."
            }
          }
        },
        "description" : "What CoreSync read from the export and what it proposes to create. Nothing has been created yet."
      },
      "MigrationZone" : {
        "type" : "object",
        "properties" : {
          "key" : {
            "type" : "string",
            "description" : "Key used in decisions.",
            "example" : "kilimani"
          },
          "name" : {
            "type" : "string",
            "description" : "Name as it will be created.",
            "example" : "Kilimani"
          },
          "subscriber_count" : {
            "type" : "integer",
            "description" : "Customers in it.",
            "format" : "int32",
            "example" : 212
          },
          "existing_zone" : {
            "type" : "string",
            "description" : "The ISP's existing zone that will be reused, if any.",
            "example" : "Kilimani"
          }
        },
        "description" : "An area in the export."
      },
      "AnalyzeMigrationRequest" : {
        "required" : [ "content_base64", "file_name" ],
        "type" : "object",
        "properties" : {
          "file_name" : {
            "maxLength" : 200,
            "minLength" : 0,
            "type" : "string",
            "description" : "The file's name, with its extension (.csv, .xlsx); the extension decides how it is read.",
            "example" : "vinea-customers.csv"
          },
          "content_base64" : {
            "type" : "string",
            "description" : "The file's bytes, base64-encoded. At most 20 MB before encoding; split bigger exports and migrate in batches."
          },
          "source" : {
            "maxLength" : 50,
            "minLength" : 0,
            "type" : "string",
            "description" : "Which old system it came from, when automatic detection should be overridden (see available_sources in the result).",
            "example" : "VINEA"
          }
        },
        "description" : "An export of the ISP's customers from their old billing system (CSV or Excel), base64-encoded."
      },
      "MigrationProblemRow" : {
        "type" : "object",
        "properties" : {
          "row_number" : {
            "type" : "integer",
            "description" : "Row in the export.",
            "format" : "int32",
            "example" : 57
          },
          "account_number" : {
            "type" : "string",
            "description" : "Account number in the export.",
            "example" : "ACC-0057"
          },
          "username" : {
            "type" : "string",
            "description" : "Username in the export.",
            "example" : "peter.k"
          },
          "name" : {
            "type" : "string",
            "description" : "Name in the export.",
            "example" : "Peter Kamau"
          },
          "outcome" : {
            "type" : "string",
            "description" : "MISSING or MISMATCHED.",
            "example" : "MISMATCHED"
          },
          "issues" : {
            "type" : "array",
            "description" : "What differs, field by field.",
            "items" : {
              "type" : "string",
              "description" : "What differs, field by field."
            }
          }
        },
        "description" : "A row that is missing or differs."
      },
      "MigrationVerification" : {
        "type" : "object",
        "properties" : {
          "clean" : {
            "type" : "boolean",
            "description" : "True when every expected customer is present and matches.",
            "example" : true
          },
          "source_rows" : {
            "type" : "integer",
            "description" : "Rows in the export.",
            "format" : "int32",
            "example" : 1240
          },
          "expected_in_system" : {
            "type" : "integer",
            "description" : "Rows that should now exist as subscribers.",
            "format" : "int32",
            "example" : 1198
          },
          "matched" : {
            "type" : "integer",
            "description" : "Present and matching.",
            "format" : "int32",
            "example" : 1198
          },
          "missing" : {
            "type" : "integer",
            "description" : "Expected but not found.",
            "format" : "int32",
            "example" : 0
          },
          "mismatched" : {
            "type" : "integer",
            "description" : "Found, but some fields differ.",
            "format" : "int32",
            "example" : 0
          },
          "intentionally_excluded" : {
            "type" : "integer",
            "description" : "Left out on purpose.",
            "format" : "int32",
            "example" : 42
          },
          "active_without_radius" : {
            "type" : "integer",
            "description" : "ACTIVE subscribers with no RADIUS login (they could not connect).",
            "format" : "int32",
            "example" : 0
          },
          "issue_counts" : {
            "type" : "object",
            "additionalProperties" : {
              "type" : "integer",
              "description" : "Problems by kind.",
              "format" : "int32"
            },
            "description" : "Problems by kind."
          },
          "headlines" : {
            "type" : "array",
            "description" : "One-line findings for people.",
            "items" : {
              "type" : "string",
              "description" : "One-line findings for people."
            }
          },
          "problems" : {
            "type" : "array",
            "description" : "The first problem rows.",
            "items" : {
              "$ref" : "#/components/schemas/MigrationProblemRow"
            }
          }
        },
        "description" : "The export checked against CoreSync, row by row."
      },
      "MigrationProgress" : {
        "type" : "object",
        "properties" : {
          "done" : {
            "type" : "boolean",
            "description" : "True once the import has finished (successfully or not).",
            "example" : false
          },
          "status" : {
            "type" : "string",
            "description" : "RUNNING, DONE or FAILED while the job is known; null after a restart.",
            "example" : "RUNNING"
          },
          "processed" : {
            "type" : "integer",
            "description" : "Rows processed so far.",
            "format" : "int32",
            "example" : 400
          },
          "total" : {
            "type" : "integer",
            "description" : "Rows to import.",
            "format" : "int32",
            "example" : 1198
          },
          "success" : {
            "type" : "integer",
            "description" : "Subscribers created.",
            "format" : "int32",
            "example" : 398
          },
          "failed" : {
            "type" : "integer",
            "description" : "Rows that failed.",
            "format" : "int32",
            "example" : 2
          },
          "errors" : {
            "type" : "array",
            "description" : "Why rows failed (first ones).",
            "items" : {
              "type" : "string",
              "description" : "Why rows failed (first ones)."
            }
          }
        },
        "description" : "How far an import has got. Poll it every few seconds until done is true."
      },
      "ApplyMigrationRequest" : {
        "type" : "object",
        "properties" : {
          "node_strategy" : {
            "pattern" : "^(NODES_AS_ZONES|NODES_AS_BUILDINGS)$",
            "type" : "string",
            "description" : "Override the plan's suggested_node_strategy.",
            "enum" : [ "NODES_AS_ZONES", "NODES_AS_BUILDINGS" ]
          },
          "packages" : {
            "type" : "array",
            "description" : "Prices and names for packages; required for any package with price_required.",
            "items" : {
              "$ref" : "#/components/schemas/MigrationPackageChoice"
            }
          },
          "nas_devices" : {
            "type" : "array",
            "description" : "Which of the ISP's routers each exported router maps to.",
            "items" : {
              "$ref" : "#/components/schemas/MigrationNasChoice"
            }
          },
          "include_terminated" : {
            "type" : "boolean",
            "description" : "Also import accounts that were terminated in the old system.",
            "example" : false
          },
          "import_balances" : {
            "type" : "boolean",
            "description" : "Carry customers' balances over into their CoreSync wallets.",
            "example" : false
          }
        },
        "description" : "Decisions for applying a migration. Anything left out follows the plan's suggestions: every zone and package is created, or the matching existing one reused, at the suggested price."
      },
      "MigrationNasChoice" : {
        "required" : [ "key" ],
        "type" : "object",
        "properties" : {
          "key" : {
            "type" : "string",
            "description" : "The router key from the plan.",
            "example" : "kilimani-ccr"
          },
          "nas_id" : {
            "type" : "string",
            "description" : "The ISP's router (NAS id) to assign; null leaves them unassigned.",
            "example" : "12"
          }
        },
        "description" : "Where one exported router's customers will authenticate."
      },
      "MigrationPackageChoice" : {
        "required" : [ "key" ],
        "type" : "object",
        "properties" : {
          "key" : {
            "type" : "string",
            "description" : "The package key from the plan.",
            "example" : "10mbps"
          },
          "price" : {
            "pattern" : "^\\d{1,9}(\\.\\d{1,2})?$",
            "type" : "string",
            "description" : "Price per cycle for the new plan, as a decimal string.",
            "example" : "2500.0"
          },
          "plan_name" : {
            "type" : "string",
            "description" : "Name for the new plan; the export's label by default.",
            "example" : "Home 10 Mbps"
          },
          "existing_plan_id" : {
            "type" : "string",
            "description" : "Use this existing plan instead of creating one (see listPlans)."
          }
        },
        "description" : "A decision for one package."
      },
      "MigrationApplied" : {
        "type" : "object",
        "properties" : {
          "migration" : {
            "$ref" : "#/components/schemas/Migration"
          },
          "zones_created" : {
            "type" : "integer",
            "description" : "Zones created.",
            "format" : "int32",
            "example" : 4
          },
          "zones_reused" : {
            "type" : "integer",
            "description" : "Existing zones reused.",
            "format" : "int32",
            "example" : 1
          },
          "buildings_created" : {
            "type" : "integer",
            "description" : "Buildings (nodes) created.",
            "format" : "int32",
            "example" : 0
          },
          "speed_profiles_created" : {
            "type" : "integer",
            "description" : "Speed profiles created.",
            "format" : "int32",
            "example" : 3
          },
          "plans_created" : {
            "type" : "integer",
            "description" : "Plans created.",
            "format" : "int32",
            "example" : 3
          },
          "plans_reused" : {
            "type" : "integer",
            "description" : "Existing plans reused.",
            "format" : "int32",
            "example" : 0
          },
          "rows_ready" : {
            "type" : "integer",
            "description" : "Subscribers that the import will create.",
            "format" : "int32",
            "example" : 1198
          },
          "rows_excluded" : {
            "type" : "integer",
            "description" : "Rows left out.",
            "format" : "int32",
            "example" : 42
          },
          "warnings" : {
            "type" : "array",
            "description" : "Things to know before importing.",
            "items" : {
              "type" : "string",
              "description" : "Things to know before importing."
            }
          },
          "errors" : {
            "type" : "array",
            "description" : "Rows that cannot be imported, and why.",
            "items" : {
              "type" : "string",
              "description" : "Rows that cannot be imported, and why."
            }
          }
        },
        "description" : "What applying created. Subscribers are created by startMigrationImport, next."
      },
      "UpdateWebhookEndpointRequest" : {
        "type" : "object",
        "properties" : {
          "url" : {
            "maxLength" : 500,
            "minLength" : 0,
            "type" : "string",
            "description" : "New HTTPS URL.",
            "example" : "https://app.getcrewbot.com/hooks/coresync"
          },
          "events" : {
            "type" : "array",
            "description" : "The full new list of events (replaces the old list).",
            "items" : {
              "type" : "string",
              "enum" : [ "payment.received", "subscriber.suspended", "subscriber.reactivated", "session.down" ]
            }
          },
          "description" : {
            "maxLength" : 200,
            "minLength" : 0,
            "type" : "string",
            "description" : "New note.",
            "example" : "crew.bot production"
          },
          "active" : {
            "type" : "boolean",
            "description" : "false switches the endpoint off; true switches it back on and clears an automatic disable.",
            "example" : true
          }
        },
        "description" : "Changes to a webhook endpoint. Only the fields you send change."
      },
      "PagedResultWebhookDelivery" : {
        "type" : "object",
        "properties" : {
          "items" : {
            "type" : "array",
            "description" : "The records on this page.",
            "items" : {
              "$ref" : "#/components/schemas/WebhookDelivery"
            }
          },
          "total" : {
            "type" : "integer",
            "description" : "How many records match, across all pages.",
            "format" : "int64",
            "example" : 137
          },
          "page" : {
            "type" : "integer",
            "description" : "This page's number, starting at 1.",
            "format" : "int32",
            "example" : 1
          },
          "size" : {
            "type" : "integer",
            "description" : "The page size that was used.",
            "format" : "int32",
            "example" : 50
          }
        },
        "description" : "One page of results. Ask for the next page with page+1 until page*size >= total."
      },
      "PagedResultTenant" : {
        "type" : "object",
        "properties" : {
          "items" : {
            "type" : "array",
            "description" : "The records on this page.",
            "items" : {
              "$ref" : "#/components/schemas/Tenant"
            }
          },
          "total" : {
            "type" : "integer",
            "description" : "How many records match, across all pages.",
            "format" : "int64",
            "example" : 137
          },
          "page" : {
            "type" : "integer",
            "description" : "This page's number, starting at 1.",
            "format" : "int32",
            "example" : 1
          },
          "size" : {
            "type" : "integer",
            "description" : "The page size that was used.",
            "format" : "int32",
            "example" : 50
          }
        },
        "description" : "One page of results. Ask for the next page with page+1 until page*size >= total."
      },
      "PagedResultSubscriberSummary" : {
        "type" : "object",
        "properties" : {
          "items" : {
            "type" : "array",
            "description" : "The records on this page.",
            "items" : {
              "$ref" : "#/components/schemas/SubscriberSummary"
            }
          },
          "total" : {
            "type" : "integer",
            "description" : "How many records match, across all pages.",
            "format" : "int64",
            "example" : 137
          },
          "page" : {
            "type" : "integer",
            "description" : "This page's number, starting at 1.",
            "format" : "int32",
            "example" : 1
          },
          "size" : {
            "type" : "integer",
            "description" : "The page size that was used.",
            "format" : "int32",
            "example" : 50
          }
        },
        "description" : "One page of results. Ask for the next page with page+1 until page*size >= total."
      },
      "SubscriberSummary" : {
        "type" : "object",
        "properties" : {
          "id" : {
            "type" : "string",
            "description" : "Subscriber id. Use it in every /subscribers/{id} operation.",
            "example" : "3f2a6c1e-8b7d-4c1a-9e2f-1a2b3c4d5e6f"
          },
          "account_number" : {
            "type" : "string",
            "description" : "The ISP's account number.",
            "example" : "ACC-00123"
          },
          "subscriber_code" : {
            "type" : "string",
            "description" : "The billing code the customer types as the M-Pesa paybill account reference.",
            "example" : "EK1234"
          },
          "username" : {
            "type" : "string",
            "description" : "PPPoE / RADIUS login name.",
            "example" : "jane.w"
          },
          "name" : {
            "type" : "string",
            "description" : "Full name, or the business name for a business account.",
            "example" : "Jane Wanjiru"
          },
          "phone" : {
            "type" : "string",
            "description" : "Primary phone number in E.164.",
            "example" : "+254712345678"
          },
          "email" : {
            "type" : "string",
            "description" : "Email address, if the ISP has one.",
            "example" : "jane@example.com"
          },
          "status" : {
            "type" : "string",
            "description" : "Account status. ACTIVE: connected and billed. SUSPENDED: service off (unpaid, expired or by hand). PENDING / DRAFT: being set up. TERMINATED: closed.",
            "example" : "ACTIVE",
            "enum" : [ "DRAFT", "PENDING", "ACTIVE", "SUSPENDED", "TERMINATED" ]
          },
          "service_type" : {
            "type" : "string",
            "description" : "How the subscriber connects.",
            "example" : "PPPOE",
            "enum" : [ "PPPOE", "HOTSPOT", "STATIC", "BOTH" ]
          },
          "online" : {
            "type" : "boolean",
            "description" : "True while the subscriber has a live session.",
            "example" : true
          },
          "plan_name" : {
            "type" : "string",
            "description" : "Name of the plan on the current subscription, if any.",
            "example" : "Home 10 Mbps"
          },
          "expires_at" : {
            "type" : "string",
            "description" : "When the current subscription runs out (UTC).",
            "format" : "date-time",
            "example" : "2026-11-25T09:00:00Z"
          },
          "credit_balance" : {
            "$ref" : "#/components/schemas/Money"
          }
        },
        "description" : "A subscriber as listed. Fetch getSubscriber for the plan, subscription and balance in full."
      },
      "Session" : {
        "type" : "object",
        "properties" : {
          "id" : {
            "type" : "string",
            "description" : "Session id (RADIUS accounting row id).",
            "example" : "4815162342"
          },
          "type" : {
            "type" : "string",
            "description" : "PPPOE or HOTSPOT.",
            "example" : "PPPOE",
            "enum" : [ "PPPOE", "HOTSPOT" ]
          },
          "username" : {
            "type" : "string",
            "description" : "RADIUS username (PPPoE login, or the voucher / MAC for hotspot).",
            "example" : "jane.w"
          },
          "subscriber" : {
            "$ref" : "#/components/schemas/SubscriberRef"
          },
          "ip_address" : {
            "type" : "string",
            "description" : "IP address assigned to the connection.",
            "example" : "10.6.0.25"
          },
          "mac_address" : {
            "type" : "string",
            "description" : "The device's MAC address, as the router reports it.",
            "example" : "AA:BB:CC:DD:EE:FF"
          },
          "nas_ip" : {
            "type" : "string",
            "description" : "The router (NAS) the session is on.",
            "example" : "10.10.0.1"
          },
          "nas_name" : {
            "type" : "string",
            "description" : "The router's name.",
            "example" : "Kilimani-CCR"
          },
          "started_at" : {
            "type" : "string",
            "description" : "When the session started (UTC).",
            "format" : "date-time",
            "example" : "2026-10-25T06:12:00Z"
          },
          "ended_at" : {
            "type" : "string",
            "description" : "When it ended (UTC); null while it is live.",
            "format" : "date-time"
          },
          "end_cause" : {
            "type" : "string",
            "description" : "Why it ended, as RADIUS reports it (e.g. Lost-Carrier, User-Request, Admin-Reset); null while live."
          },
          "duration_seconds" : {
            "type" : "integer",
            "description" : "Length of the session so far, in seconds.",
            "format" : "int64",
            "example" : 9840
          },
          "bytes_up" : {
            "type" : "integer",
            "description" : "Bytes the subscriber uploaded in this session.",
            "format" : "int64",
            "example" : 104857600
          },
          "bytes_down" : {
            "type" : "integer",
            "description" : "Bytes the subscriber downloaded in this session.",
            "format" : "int64",
            "example" : 2147483648
          }
        },
        "description" : "A RADIUS session: one connection of a subscriber (PPPoE) or a hotspot device."
      },
      "SubscriberSession" : {
        "type" : "object",
        "properties" : {
          "online" : {
            "type" : "boolean",
            "description" : "True when the subscriber has a live session.",
            "example" : true
          },
          "current" : {
            "$ref" : "#/components/schemas/Session"
          },
          "last_session" : {
            "$ref" : "#/components/schemas/Session"
          }
        },
        "description" : "Whether a subscriber is online right now, with their live session or their last one."
      },
      "PagedResultPayment" : {
        "type" : "object",
        "properties" : {
          "items" : {
            "type" : "array",
            "description" : "The records on this page.",
            "items" : {
              "$ref" : "#/components/schemas/Payment"
            }
          },
          "total" : {
            "type" : "integer",
            "description" : "How many records match, across all pages.",
            "format" : "int64",
            "example" : 137
          },
          "page" : {
            "type" : "integer",
            "description" : "This page's number, starting at 1.",
            "format" : "int32",
            "example" : 1
          },
          "size" : {
            "type" : "integer",
            "description" : "The page size that was used.",
            "format" : "int32",
            "example" : 50
          }
        },
        "description" : "One page of results. Ask for the next page with page+1 until page*size >= total."
      },
      "Invoice" : {
        "type" : "object",
        "properties" : {
          "id" : {
            "type" : "string",
            "description" : "Invoice id.",
            "example" : "7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d"
          },
          "number" : {
            "type" : "string",
            "description" : "Invoice number as printed.",
            "example" : "INV-202610-0042"
          },
          "subscriber" : {
            "$ref" : "#/components/schemas/SubscriberRef"
          },
          "plan_name" : {
            "type" : "string",
            "description" : "The plan billed, as it was named when the invoice was issued.",
            "example" : "Home 10 Mbps"
          },
          "amount" : {
            "$ref" : "#/components/schemas/Money"
          },
          "status" : {
            "type" : "string",
            "description" : "ISSUED: unpaid. OVERDUE: unpaid past the due date. PAID. VOID: cancelled.",
            "example" : "ISSUED",
            "enum" : [ "ISSUED", "PAID", "OVERDUE", "VOID" ]
          },
          "issued_on" : {
            "type" : "string",
            "description" : "Issue date (ISO 8601 date).",
            "format" : "date",
            "example" : "2026-10-01"
          },
          "due_on" : {
            "type" : "string",
            "description" : "Due date (ISO 8601 date).",
            "format" : "date",
            "example" : "2026-10-08"
          },
          "paid_on" : {
            "type" : "string",
            "description" : "Date it was paid, if it was.",
            "format" : "date"
          },
          "payment_id" : {
            "type" : "string",
            "description" : "The payment that settled it, if any."
          },
          "payment_reference" : {
            "type" : "string",
            "description" : "Receipt / reference of the settling payment."
          },
          "created_at" : {
            "type" : "string",
            "description" : "When the invoice record was created (UTC).",
            "format" : "date-time",
            "example" : "2026-10-01T05:00:00Z"
          }
        },
        "description" : "An invoice issued to a subscriber."
      },
      "PagedResultInvoice" : {
        "type" : "object",
        "properties" : {
          "items" : {
            "type" : "array",
            "description" : "The records on this page.",
            "items" : {
              "$ref" : "#/components/schemas/Invoice"
            }
          },
          "total" : {
            "type" : "integer",
            "description" : "How many records match, across all pages.",
            "format" : "int64",
            "example" : 137
          },
          "page" : {
            "type" : "integer",
            "description" : "This page's number, starting at 1.",
            "format" : "int32",
            "example" : 1
          },
          "size" : {
            "type" : "integer",
            "description" : "The page size that was used.",
            "format" : "int32",
            "example" : 50
          }
        },
        "description" : "One page of results. Ask for the next page with page+1 until page*size >= total."
      },
      "PagedResultSession" : {
        "type" : "object",
        "properties" : {
          "items" : {
            "type" : "array",
            "description" : "The records on this page.",
            "items" : {
              "$ref" : "#/components/schemas/Session"
            }
          },
          "total" : {
            "type" : "integer",
            "description" : "How many records match, across all pages.",
            "format" : "int64",
            "example" : 137
          },
          "page" : {
            "type" : "integer",
            "description" : "This page's number, starting at 1.",
            "format" : "int32",
            "example" : 1
          },
          "size" : {
            "type" : "integer",
            "description" : "The page size that was used.",
            "format" : "int32",
            "example" : 50
          }
        },
        "description" : "One page of results. Ask for the next page with page+1 until page*size >= total."
      },
      "PagedResultPlan" : {
        "type" : "object",
        "properties" : {
          "items" : {
            "type" : "array",
            "description" : "The records on this page.",
            "items" : {
              "$ref" : "#/components/schemas/Plan"
            }
          },
          "total" : {
            "type" : "integer",
            "description" : "How many records match, across all pages.",
            "format" : "int64",
            "example" : 137
          },
          "page" : {
            "type" : "integer",
            "description" : "This page's number, starting at 1.",
            "format" : "int32",
            "example" : 1
          },
          "size" : {
            "type" : "integer",
            "description" : "The page size that was used.",
            "format" : "int32",
            "example" : 50
          }
        },
        "description" : "One page of results. Ask for the next page with page+1 until page*size >= total."
      },
      "Plan" : {
        "type" : "object",
        "properties" : {
          "id" : {
            "type" : "string",
            "description" : "Plan id.",
            "example" : "9c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f"
          },
          "code" : {
            "type" : "string",
            "description" : "Plan code.",
            "example" : "HOME10"
          },
          "name" : {
            "type" : "string",
            "description" : "Plan name as customers see it.",
            "example" : "Home 10 Mbps"
          },
          "description" : {
            "type" : "string",
            "description" : "Free-text description.",
            "example" : "Unlimited home fibre, 10 Mbps"
          },
          "type" : {
            "type" : "string",
            "description" : "FIXED: a home/business line (PPPoE or static). HOTSPOT: a WiFi voucher package.",
            "example" : "FIXED",
            "enum" : [ "FIXED", "HOTSPOT" ]
          },
          "price" : {
            "$ref" : "#/components/schemas/Money"
          },
          "validity_value" : {
            "type" : "integer",
            "description" : "Length of one cycle, in validity_unit.",
            "format" : "int32",
            "example" : 30
          },
          "validity_unit" : {
            "type" : "string",
            "description" : "Unit of the cycle length.",
            "example" : "DAYS",
            "enum" : [ "MINUTES", "HOURS", "DAYS", "WEEKS", "MONTHS", "YEARS" ]
          },
          "download_mbps" : {
            "type" : "number",
            "description" : "Download speed in Mbps, when the plan has a speed profile.",
            "format" : "double",
            "example" : 10
          },
          "upload_mbps" : {
            "type" : "number",
            "description" : "Upload speed in Mbps, when the plan has a speed profile.",
            "format" : "double",
            "example" : 5
          },
          "data_limit_bytes" : {
            "type" : "integer",
            "description" : "Data cap per cycle in bytes; null means unlimited.",
            "format" : "int64"
          },
          "max_devices" : {
            "type" : "integer",
            "description" : "How many devices may be online at once; null uses the ISP default.",
            "format" : "int32",
            "example" : 1
          },
          "status" : {
            "type" : "string",
            "description" : "Plan status. Only ACTIVE plans can be assigned.",
            "example" : "ACTIVE",
            "enum" : [ "ACTIVE", "INACTIVE", "ARCHIVED" ]
          },
          "visible" : {
            "type" : "boolean",
            "description" : "Whether customers see the plan on self-service pages.",
            "example" : true
          },
          "created_at" : {
            "type" : "string",
            "description" : "When the plan was created (UTC).",
            "format" : "date-time",
            "example" : "2026-01-10T07:00:00Z"
          }
        },
        "description" : "A service plan (package) the ISP sells."
      },
      "PagedResultMigration" : {
        "type" : "object",
        "properties" : {
          "items" : {
            "type" : "array",
            "description" : "The records on this page.",
            "items" : {
              "$ref" : "#/components/schemas/Migration"
            }
          },
          "total" : {
            "type" : "integer",
            "description" : "How many records match, across all pages.",
            "format" : "int64",
            "example" : 137
          },
          "page" : {
            "type" : "integer",
            "description" : "This page's number, starting at 1.",
            "format" : "int32",
            "example" : 1
          },
          "size" : {
            "type" : "integer",
            "description" : "The page size that was used.",
            "format" : "int32",
            "example" : 50
          }
        },
        "description" : "One page of results. Ask for the next page with page+1 until page*size >= total."
      },
      "Account" : {
        "type" : "object",
        "properties" : {
          "key" : {
            "$ref" : "#/components/schemas/ApiKeyInfo"
          },
          "tenant" : {
            "$ref" : "#/components/schemas/Tenant"
          }
        },
        "description" : "Who you are calling as: the API key, and the ISP it acts on."
      },
      "ApiKeyInfo" : {
        "type" : "object",
        "properties" : {
          "name" : {
            "type" : "string",
            "description" : "The key's name; it is the actor on everything the key does.",
            "example" : "crew.bot"
          },
          "prefix" : {
            "type" : "string",
            "description" : "The first characters of the key, to tell keys apart.",
            "example" : "cs_live_Ab3d"
          },
          "kind" : {
            "type" : "string",
            "description" : "TENANT: belongs to one ISP. PLATFORM: an Avitech key that names the ISP in X-Tenant-Code.",
            "example" : "TENANT",
            "enum" : [ "TENANT", "PLATFORM" ]
          },
          "scopes" : {
            "type" : "array",
            "description" : "What the key may do.",
            "example" : [ "read", "write" ],
            "items" : {
              "type" : "string",
              "description" : "What the key may do.",
              "example" : "[\"read\",\"write\"]"
            }
          },
          "rate_limit_per_minute" : {
            "type" : "integer",
            "description" : "Requests allowed per minute.",
            "format" : "int32",
            "example" : 120
          },
          "expires_at" : {
            "type" : "string",
            "description" : "When the key stops working (UTC); null if it does not expire.",
            "format" : "date-time"
          }
        },
        "description" : "An API key, as the API sees it. The secret itself is never returned."
      },
      "Problem" : {
        "required" : [ "code", "status", "title", "type" ],
        "type" : "object",
        "properties" : {
          "type" : {
            "type" : "string",
            "description" : "urn:problem:<code>.",
            "example" : "urn:problem:not_found"
          },
          "title" : {
            "type" : "string",
            "description" : "Short summary of the kind of problem.",
            "example" : "Not found"
          },
          "status" : {
            "type" : "integer",
            "description" : "HTTP status.",
            "format" : "int32",
            "example" : 404
          },
          "detail" : {
            "type" : "string",
            "description" : "What went wrong, for a person.",
            "example" : "Subscriber 3f2a… was not found."
          },
          "instance" : {
            "type" : "string",
            "description" : "The request path.",
            "example" : "/api/v1/subscribers/3f2a6c1e-8b7d-4c1a-9e2f-1a2b3c4d5e6f"
          },
          "code" : {
            "type" : "string",
            "description" : "Stable machine-readable code.",
            "example" : "not_found"
          },
          "errors" : {
            "type" : "array",
            "description" : "Per-field problems, for validation_failed.",
            "items" : {
              "type" : "object",
              "properties" : {
                "field" : {
                  "type" : "string",
                  "description" : "The field or parameter.",
                  "example" : "amount"
                },
                "message" : {
                  "type" : "string",
                  "description" : "What is wrong with it.",
                  "example" : "must not be null"
                }
              }
            }
          }
        },
        "description" : "An error, as RFC 9457 problem details. Branch on code; detail is for people."
      },
      "PaymentReceivedData" : {
        "type" : "object",
        "properties" : {
          "payment" : {
            "$ref" : "#/components/schemas/Payment"
          }
        },
        "description" : "A payment that was received and applied."
      },
      "PaymentReceivedEvent" : {
        "type" : "object",
        "properties" : {
          "id" : {
            "type" : "string",
            "description" : "Event id, unique. The same event delivered twice keeps its id: use it to ignore duplicates.",
            "example" : "evt_1f2e3d4c5b6a7980"
          },
          "type" : {
            "type" : "string",
            "description" : "Event type.",
            "example" : "payment.received",
            "enum" : [ "payment.received" ]
          },
          "occurred_at" : {
            "type" : "string",
            "description" : "When it happened (UTC).",
            "format" : "date-time",
            "example" : "2026-10-25T09:00:05Z"
          },
          "tenant" : {
            "type" : "string",
            "description" : "The ISP's code.",
            "example" : "EIKAL"
          },
          "data" : {
            "$ref" : "#/components/schemas/PaymentReceivedData"
          }
        },
        "description" : "Sent when a payment is received and applied to a subscriber."
      },
      "SubscriberStatusData" : {
        "type" : "object",
        "properties" : {
          "subscriber" : {
            "$ref" : "#/components/schemas/SubscriberRef"
          },
          "status" : {
            "type" : "string",
            "description" : "Account status. ACTIVE: connected and billed. SUSPENDED: service off (unpaid, expired or by hand). PENDING / DRAFT: being set up. TERMINATED: closed.",
            "example" : "SUSPENDED",
            "enum" : [ "DRAFT", "PENDING", "ACTIVE", "SUSPENDED", "TERMINATED" ]
          },
          "previous_status" : {
            "type" : "string",
            "description" : "Account status. ACTIVE: connected and billed. SUSPENDED: service off (unpaid, expired or by hand). PENDING / DRAFT: being set up. TERMINATED: closed.",
            "example" : "ACTIVE",
            "enum" : [ "DRAFT", "PENDING", "ACTIVE", "SUSPENDED", "TERMINATED" ]
          },
          "changed_by" : {
            "type" : "string",
            "description" : "Who made the change: a staff member's name, an API key's name, or SYSTEM for automation and expiry.",
            "example" : "SYSTEM"
          }
        },
        "description" : "A subscriber whose service was switched off or back on."
      },
      "SubscriberStatusEvent" : {
        "type" : "object",
        "properties" : {
          "id" : {
            "type" : "string",
            "description" : "Event id, unique.",
            "example" : "evt_2a3b4c5d6e7f8091"
          },
          "type" : {
            "type" : "string",
            "description" : "Event type.",
            "example" : "subscriber.suspended",
            "enum" : [ "subscriber.suspended", "subscriber.reactivated" ]
          },
          "occurred_at" : {
            "type" : "string",
            "description" : "When it happened (UTC).",
            "format" : "date-time",
            "example" : "2026-10-25T21:00:00Z"
          },
          "tenant" : {
            "type" : "string",
            "description" : "The ISP's code.",
            "example" : "EIKAL"
          },
          "data" : {
            "$ref" : "#/components/schemas/SubscriberStatusData"
          }
        },
        "description" : "Sent when a subscriber is suspended (type subscriber.suspended) or reactivated (type subscriber.reactivated)."
      },
      "SessionDownData" : {
        "type" : "object",
        "properties" : {
          "session" : {
            "$ref" : "#/components/schemas/Session"
          }
        },
        "description" : "A PPPoE session that ended."
      },
      "SessionDownEvent" : {
        "type" : "object",
        "properties" : {
          "id" : {
            "type" : "string",
            "description" : "Event id, unique per session.",
            "example" : "evt_sd_4815162342"
          },
          "type" : {
            "type" : "string",
            "description" : "Event type.",
            "example" : "session.down",
            "enum" : [ "session.down" ]
          },
          "occurred_at" : {
            "type" : "string",
            "description" : "When the session ended (UTC).",
            "format" : "date-time",
            "example" : "2026-10-25T08:41:13Z"
          },
          "tenant" : {
            "type" : "string",
            "description" : "The ISP's code.",
            "example" : "EIKAL"
          },
          "data" : {
            "$ref" : "#/components/schemas/SessionDownData"
          }
        },
        "description" : "Sent when a subscriber's PPPoE session ends."
      },
      "WebhookTestData" : {
        "type" : "object",
        "properties" : {
          "message" : {
            "type" : "string",
            "description" : "Always the same text.",
            "example" : "This is a test event from CoreSync."
          },
          "endpoint_id" : {
            "type" : "string",
            "description" : "The endpoint that was tested."
          }
        },
        "description" : "A test event."
      },
      "WebhookTestEvent" : {
        "type" : "object",
        "properties" : {
          "id" : {
            "type" : "string",
            "description" : "Event id.",
            "example" : "evt_test_9a8b7c6d"
          },
          "type" : {
            "type" : "string",
            "description" : "Event type.",
            "example" : "webhook.test",
            "enum" : [ "webhook.test" ]
          },
          "occurred_at" : {
            "type" : "string",
            "description" : "When it was sent (UTC).",
            "format" : "date-time",
            "example" : "2026-10-25T09:30:00Z"
          },
          "tenant" : {
            "type" : "string",
            "description" : "The ISP's code.",
            "example" : "EIKAL"
          },
          "data" : {
            "$ref" : "#/components/schemas/WebhookTestData"
          }
        },
        "description" : "Sent by \"Send a test event\"."
      }
    },
    "parameters" : {
      "TenantCode" : {
        "name" : "X-Tenant-Code",
        "in" : "header",
        "description" : "Platform keys only: the code of the ISP to act on (see listTenants). Ignored for an ISP's own key, whose ISP is fixed.",
        "required" : false,
        "schema" : {
          "type" : "string",
          "example" : "EIKAL"
        }
      }
    },
    "headers" : {
      "X-RateLimit-Limit" : {
        "description" : "Requests this key may make per minute.",
        "schema" : {
          "type" : "integer",
          "format" : "int32",
          "example" : 120
        }
      },
      "X-RateLimit-Remaining" : {
        "description" : "Requests left in the current minute.",
        "schema" : {
          "type" : "integer",
          "format" : "int32",
          "example" : 117
        }
      },
      "Retry-After" : {
        "description" : "Seconds to wait before retrying.",
        "schema" : {
          "type" : "integer",
          "format" : "int32",
          "example" : 12
        }
      }
    },
    "securitySchemes" : {
      "ApiKeyAuth" : {
        "type" : "apiKey",
        "description" : "An API key: cs_live_… (an ISP's own key) or cs_plat_… (an Avitech platform key).",
        "name" : "X-API-Key",
        "in" : "header"
      }
    }
  },
  "webhooks" : {
    "payment.received" : {
      "post" : {
        "operationId" : "paymentReceivedWebhook",
        "summary" : "A payment was received",
        "description" : "A payment was received and applied to a subscriber (M-Pesa, bank, cash or one recorded through the API). POSTed to every endpoint subscribed to payment.received. Verify X-Webhook-Signature before trusting it, ignore an event id you have already handled, and answer 2xx within 10 seconds; anything else is retried (5 attempts: now, +5 s, +25 s, +2 min, +10 min).",
        "tags" : [ "Webhooks" ],
        "parameters" : [ {
          "name" : "X-Webhook-Event",
          "in" : "header",
          "required" : true,
          "description" : "The event type.",
          "schema" : {
            "type" : "string",
            "example" : "payment.received"
          }
        }, {
          "name" : "X-Webhook-Delivery-Id",
          "in" : "header",
          "required" : true,
          "description" : "This delivery's id (differs per endpoint and per event).",
          "schema" : {
            "type" : "string",
            "example" : "8d9e0f1a-2b3c-4d5e-6f7a-8b9c0d1e2f3a"
          }
        }, {
          "name" : "X-Webhook-Timestamp",
          "in" : "header",
          "required" : true,
          "description" : "Unix seconds when this attempt was signed. Reject anything older than 5 minutes.",
          "schema" : {
            "type" : "string",
            "example" : "1761382805"
          }
        }, {
          "name" : "X-Webhook-Signature",
          "in" : "header",
          "required" : true,
          "description" : "t=<timestamp>,v1=<hex HMAC-SHA256 of \"<timestamp>.<raw body>\" keyed with the endpoint's signing secret>.",
          "schema" : {
            "type" : "string",
            "example" : "t=1761382805,v1=5f1c...e9"
          }
        } ],
        "requestBody" : {
          "required" : true,
          "content" : {
            "application/json" : {
              "schema" : {
                "$ref" : "#/components/schemas/PaymentReceivedEvent"
              }
            }
          }
        },
        "responses" : {
          "200" : {
            "description" : "Received. Any 2xx status acknowledges the event."
          }
        }
      }
    },
    "subscriber.suspended" : {
      "post" : {
        "operationId" : "subscriberSuspendedWebhook",
        "summary" : "A subscriber was suspended",
        "description" : "A subscriber's service was suspended, by staff, by an API key, by automation, or because their subscription expired. POSTed to every endpoint subscribed to subscriber.suspended. Verify X-Webhook-Signature before trusting it, ignore an event id you have already handled, and answer 2xx within 10 seconds; anything else is retried (5 attempts: now, +5 s, +25 s, +2 min, +10 min).",
        "tags" : [ "Webhooks" ],
        "parameters" : [ {
          "name" : "X-Webhook-Event",
          "in" : "header",
          "required" : true,
          "description" : "The event type.",
          "schema" : {
            "type" : "string",
            "example" : "subscriber.suspended"
          }
        }, {
          "name" : "X-Webhook-Delivery-Id",
          "in" : "header",
          "required" : true,
          "description" : "This delivery's id (differs per endpoint and per event).",
          "schema" : {
            "type" : "string",
            "example" : "8d9e0f1a-2b3c-4d5e-6f7a-8b9c0d1e2f3a"
          }
        }, {
          "name" : "X-Webhook-Timestamp",
          "in" : "header",
          "required" : true,
          "description" : "Unix seconds when this attempt was signed. Reject anything older than 5 minutes.",
          "schema" : {
            "type" : "string",
            "example" : "1761382805"
          }
        }, {
          "name" : "X-Webhook-Signature",
          "in" : "header",
          "required" : true,
          "description" : "t=<timestamp>,v1=<hex HMAC-SHA256 of \"<timestamp>.<raw body>\" keyed with the endpoint's signing secret>.",
          "schema" : {
            "type" : "string",
            "example" : "t=1761382805,v1=5f1c...e9"
          }
        } ],
        "requestBody" : {
          "required" : true,
          "content" : {
            "application/json" : {
              "schema" : {
                "$ref" : "#/components/schemas/SubscriberStatusEvent"
              }
            }
          }
        },
        "responses" : {
          "200" : {
            "description" : "Received. Any 2xx status acknowledges the event."
          }
        }
      }
    },
    "subscriber.reactivated" : {
      "post" : {
        "operationId" : "subscriberReactivatedWebhook",
        "summary" : "A subscriber was reactivated",
        "description" : "A suspended subscriber's service was turned back on, after a payment, by staff or by an API key. POSTed to every endpoint subscribed to subscriber.reactivated. Verify X-Webhook-Signature before trusting it, ignore an event id you have already handled, and answer 2xx within 10 seconds; anything else is retried (5 attempts: now, +5 s, +25 s, +2 min, +10 min).",
        "tags" : [ "Webhooks" ],
        "parameters" : [ {
          "name" : "X-Webhook-Event",
          "in" : "header",
          "required" : true,
          "description" : "The event type.",
          "schema" : {
            "type" : "string",
            "example" : "subscriber.reactivated"
          }
        }, {
          "name" : "X-Webhook-Delivery-Id",
          "in" : "header",
          "required" : true,
          "description" : "This delivery's id (differs per endpoint and per event).",
          "schema" : {
            "type" : "string",
            "example" : "8d9e0f1a-2b3c-4d5e-6f7a-8b9c0d1e2f3a"
          }
        }, {
          "name" : "X-Webhook-Timestamp",
          "in" : "header",
          "required" : true,
          "description" : "Unix seconds when this attempt was signed. Reject anything older than 5 minutes.",
          "schema" : {
            "type" : "string",
            "example" : "1761382805"
          }
        }, {
          "name" : "X-Webhook-Signature",
          "in" : "header",
          "required" : true,
          "description" : "t=<timestamp>,v1=<hex HMAC-SHA256 of \"<timestamp>.<raw body>\" keyed with the endpoint's signing secret>.",
          "schema" : {
            "type" : "string",
            "example" : "t=1761382805,v1=5f1c...e9"
          }
        } ],
        "requestBody" : {
          "required" : true,
          "content" : {
            "application/json" : {
              "schema" : {
                "$ref" : "#/components/schemas/SubscriberStatusEvent"
              }
            }
          }
        },
        "responses" : {
          "200" : {
            "description" : "Received. Any 2xx status acknowledges the event."
          }
        }
      }
    },
    "session.down" : {
      "post" : {
        "operationId" : "sessionDownWebhook",
        "summary" : "A subscriber's session ended",
        "description" : "A subscriber's PPPoE session ended (they went offline). Sent within about a minute of the router reporting it. POSTed to every endpoint subscribed to session.down. Verify X-Webhook-Signature before trusting it, ignore an event id you have already handled, and answer 2xx within 10 seconds; anything else is retried (5 attempts: now, +5 s, +25 s, +2 min, +10 min).",
        "tags" : [ "Webhooks" ],
        "parameters" : [ {
          "name" : "X-Webhook-Event",
          "in" : "header",
          "required" : true,
          "description" : "The event type.",
          "schema" : {
            "type" : "string",
            "example" : "session.down"
          }
        }, {
          "name" : "X-Webhook-Delivery-Id",
          "in" : "header",
          "required" : true,
          "description" : "This delivery's id (differs per endpoint and per event).",
          "schema" : {
            "type" : "string",
            "example" : "8d9e0f1a-2b3c-4d5e-6f7a-8b9c0d1e2f3a"
          }
        }, {
          "name" : "X-Webhook-Timestamp",
          "in" : "header",
          "required" : true,
          "description" : "Unix seconds when this attempt was signed. Reject anything older than 5 minutes.",
          "schema" : {
            "type" : "string",
            "example" : "1761382805"
          }
        }, {
          "name" : "X-Webhook-Signature",
          "in" : "header",
          "required" : true,
          "description" : "t=<timestamp>,v1=<hex HMAC-SHA256 of \"<timestamp>.<raw body>\" keyed with the endpoint's signing secret>.",
          "schema" : {
            "type" : "string",
            "example" : "t=1761382805,v1=5f1c...e9"
          }
        } ],
        "requestBody" : {
          "required" : true,
          "content" : {
            "application/json" : {
              "schema" : {
                "$ref" : "#/components/schemas/SessionDownEvent"
              }
            }
          }
        },
        "responses" : {
          "200" : {
            "description" : "Received. Any 2xx status acknowledges the event."
          }
        }
      }
    },
    "webhook.test" : {
      "post" : {
        "operationId" : "webhookTestWebhook",
        "summary" : "A test event",
        "description" : "Sent only by \"Send a test event\". Every endpoint receives it, whatever it subscribes to. POSTed to every endpoint subscribed to webhook.test. Verify X-Webhook-Signature before trusting it, ignore an event id you have already handled, and answer 2xx within 10 seconds; anything else is retried (5 attempts: now, +5 s, +25 s, +2 min, +10 min).",
        "tags" : [ "Webhooks" ],
        "parameters" : [ {
          "name" : "X-Webhook-Event",
          "in" : "header",
          "required" : true,
          "description" : "The event type.",
          "schema" : {
            "type" : "string",
            "example" : "webhook.test"
          }
        }, {
          "name" : "X-Webhook-Delivery-Id",
          "in" : "header",
          "required" : true,
          "description" : "This delivery's id (differs per endpoint and per event).",
          "schema" : {
            "type" : "string",
            "example" : "8d9e0f1a-2b3c-4d5e-6f7a-8b9c0d1e2f3a"
          }
        }, {
          "name" : "X-Webhook-Timestamp",
          "in" : "header",
          "required" : true,
          "description" : "Unix seconds when this attempt was signed. Reject anything older than 5 minutes.",
          "schema" : {
            "type" : "string",
            "example" : "1761382805"
          }
        }, {
          "name" : "X-Webhook-Signature",
          "in" : "header",
          "required" : true,
          "description" : "t=<timestamp>,v1=<hex HMAC-SHA256 of \"<timestamp>.<raw body>\" keyed with the endpoint's signing secret>.",
          "schema" : {
            "type" : "string",
            "example" : "t=1761382805,v1=5f1c...e9"
          }
        } ],
        "requestBody" : {
          "required" : true,
          "content" : {
            "application/json" : {
              "schema" : {
                "$ref" : "#/components/schemas/WebhookTestEvent"
              }
            }
          }
        },
        "responses" : {
          "200" : {
            "description" : "Received. Any 2xx status acknowledges the event."
          }
        }
      }
    }
  }
}