openapi: 3.1.0 info: title: AS213905 API version: 1.2.0 description: | Customer automation API for organisations, invoices, services, traffic, tunnels and RFC 8805 geofeeds. Create a scoped key in Panel → Settings. contact: name: AS213905 NOC email: noc@as213905.com url: https://as213905.com/docs/api license: name: Proprietary API; SDKs licensed under MIT url: https://git.datacoria.com/Phylex/as213905-sdk/src/branch/main/LICENSE servers: - url: https://as213905.com tags: - name: Organisation description: The organisation bound to the API key. - name: Invoices description: Billing documents and payment state. - name: Services description: Customer services, BGP VM metrics and power actions. - name: Traffic description: Organisation and per-tunnel traffic accounting. - name: Tunnels description: Transit tunnel inventory and lifecycle. - name: Geofeeds description: RFC 8805 public geofeed management. security: - apiKey: [] paths: /api/v1/organization: get: tags: [Organisation] summary: Get the API key's organisation description: Returns the single organisation permanently bound to the bearer key. Requires `organizations:read`. operationId: getOrganization x-required-scope: organizations:read responses: '200': description: Organisation content: application/json: schema: {$ref: '#/components/schemas/OrganizationResponse'} '401': {$ref: '#/components/responses/Unauthenticated'} '403': {$ref: '#/components/responses/Forbidden'} /api/v1/invoices: get: tags: [Invoices] summary: List invoices, newest first description: Returns all billing documents for the key organisation, ordered from newest to oldest. Requires `invoices:read`. operationId: listInvoices x-required-scope: invoices:read responses: '200': description: Invoice list content: application/json: schema: type: object required: [invoices] properties: invoices: {type: array, items: {$ref: '#/components/schemas/Invoice'}} '401': {$ref: '#/components/responses/Unauthenticated'} '403': {$ref: '#/components/responses/Forbidden'} /api/v1/services: get: tags: [Services] summary: List services description: Returns every customer service and its lifecycle state. Requires `services:read`. operationId: listServices x-required-scope: services:read responses: '200': description: Service list content: application/json: schema: type: object required: [services] properties: services: {type: array, items: {$ref: '#/components/schemas/Service'}} '401': {$ref: '#/components/responses/Unauthenticated'} '403': {$ref: '#/components/responses/Forbidden'} /api/v1/services/{id}: parameters: - {$ref: '#/components/parameters/ResourceId'} get: tags: [Services] summary: Get one service description: Returns one service owned by the key organisation. A foreign identifier is reported as not found. Requires `services:read`. operationId: getService x-required-scope: services:read responses: '200': description: Service content: application/json: schema: type: object required: [service] properties: service: {$ref: '#/components/schemas/Service'} '401': {$ref: '#/components/responses/Unauthenticated'} '403': {$ref: '#/components/responses/Forbidden'} '404': {$ref: '#/components/responses/NotFound'} /api/v1/services/{id}/metrics: parameters: - {$ref: '#/components/parameters/ResourceId'} get: tags: [Services] summary: Read a BGP VM metric series description: Reads a sampled metric series from a controllable BGP VM. Requires `services:read`. operationId: getServiceMetrics x-required-scope: services:read parameters: - name: target in: query description: Metric to read. Network values are returned as bytes per second; percentage and load values use their native units. example: net_tx schema: type: string enum: [cpu_load, mem_usage_percent, net_rx, net_tx, iops, df.root.used] default: cpu_load - name: range in: query description: Time window ending at the current time. example: 6h schema: {type: string, enum: [1h, 6h, 24h, 7d], default: 24h} responses: '200': description: Metric series content: application/json: schema: {$ref: '#/components/schemas/MetricSeries'} '401': {$ref: '#/components/responses/Unauthenticated'} '403': {$ref: '#/components/responses/Forbidden'} '404': {$ref: '#/components/responses/NotFound'} /api/v1/services/{id}/power: parameters: - {$ref: '#/components/parameters/ResourceId'} post: tags: [Services] summary: Start, stop or restart a BGP VM description: Queues an asynchronous power command for a controllable BGP VM. Requires `services:write`. operationId: powerService x-required-scope: services:write requestBody: required: true content: application/json: schema: type: object required: [action] properties: action: {type: string, enum: [start, stop, restart], examples: [restart], description: Power transition to queue.} examples: restart: {value: {action: restart}} responses: '202': description: Command accepted content: application/json: schema: type: object required: [status, action] properties: status: {type: string, const: accepted} action: {type: string, enum: [start, stop, restart]} '401': {$ref: '#/components/responses/Unauthenticated'} '403': {$ref: '#/components/responses/Forbidden'} '404': {$ref: '#/components/responses/NotFound'} '429': {$ref: '#/components/responses/RateLimited'} /api/v1/traffic: get: tags: [Traffic] summary: Get organisation transit usage description: Returns transferred bytes and 95th-percentile traffic for either the last N days or an explicit UTC date interval. `days` is mutually exclusive with `from`/`to`. Requires `traffic:read`. operationId: getTraffic x-required-scope: traffic:read parameters: - {$ref: '#/components/parameters/Days'} - {$ref: '#/components/parameters/From'} - {$ref: '#/components/parameters/To'} responses: '200': description: Traffic summary content: application/json: schema: {$ref: '#/components/schemas/Traffic'} '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthenticated'} '403': {$ref: '#/components/responses/Forbidden'} /api/v1/tunnels: get: tags: [Tunnels] summary: List tunnels description: Returns all transit tunnels and their provisioning state. Requires `tunnels:read`. operationId: listTunnels x-required-scope: tunnels:read responses: '200': description: Tunnel list content: application/json: schema: type: object required: [tunnels] properties: tunnels: {type: array, items: {$ref: '#/components/schemas/Tunnel'}} '401': {$ref: '#/components/responses/Unauthenticated'} '403': {$ref: '#/components/responses/Forbidden'} post: tags: [Tunnels] summary: Request a transit tunnel description: Records a Frankfurt transit tunnel for asynchronous provisioning. Currently `fra` is the only available location. Requires `tunnels:write`. operationId: createTunnel x-required-scope: tunnels:write requestBody: required: true content: application/json: schema: {$ref: '#/components/schemas/CreateTunnelRequest'} examples: frankfurt: summary: Frankfurt GRE tunnel value: {peer_endpoint: 203.0.113.10, location: fra, label: fra-primary, announce: true} responses: '202': description: Tunnel recorded for provisioning content: application/json: schema: type: object required: [tunnel] properties: tunnel: {$ref: '#/components/schemas/Tunnel'} '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthenticated'} '403': {$ref: '#/components/responses/Forbidden'} /api/v1/tunnels/{id}: parameters: - {$ref: '#/components/parameters/ResourceId'} delete: tags: [Tunnels] summary: Remove a tunnel asynchronously description: Queues teardown and returns before router configuration has completed. Requires `tunnels:write`. operationId: deleteTunnel x-required-scope: tunnels:write responses: '202': description: Removal accepted content: application/json: schema: {$ref: '#/components/schemas/StatusResponse'} '401': {$ref: '#/components/responses/Unauthenticated'} '403': {$ref: '#/components/responses/Forbidden'} '404': {$ref: '#/components/responses/NotFound'} /api/v1/tunnels/{id}/traffic: parameters: - {$ref: '#/components/parameters/ResourceId'} get: tags: [Traffic, Tunnels] summary: Get one tunnel's usage description: Returns transferred bytes and 95th-percentile traffic for one tunnel. `days` is mutually exclusive with `from`/`to`. Requires `traffic:read`. operationId: getTunnelTraffic x-required-scope: traffic:read parameters: - {$ref: '#/components/parameters/Days'} - {$ref: '#/components/parameters/From'} - {$ref: '#/components/parameters/To'} responses: '200': description: Tunnel traffic content: application/json: schema: {$ref: '#/components/schemas/TunnelTraffic'} '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthenticated'} '403': {$ref: '#/components/responses/Forbidden'} '404': {$ref: '#/components/responses/NotFound'} /api/v1/geofeeds: parameters: - {$ref: '#/components/parameters/OrganizationId'} get: tags: [Geofeeds] summary: List geofeeds description: Lists the organisation's public RFC 8805 feeds. Each organisation can own at most two. Requires `geofeeds:read`. operationId: listGeofeeds x-required-scope: geofeeds:read responses: '200': description: Geofeed list content: application/json: schema: {$ref: '#/components/schemas/GeofeedList'} '401': {$ref: '#/components/responses/Unauthenticated'} '403': {$ref: '#/components/responses/Forbidden'} post: tags: [Geofeeds] summary: Create a geofeed (maximum two per organisation) description: Creates an empty feed and an unguessable public CSV URL. Requires `geofeeds:write`. operationId: createGeofeed x-required-scope: geofeeds:write requestBody: required: true content: application/json: schema: type: object required: [name] properties: name: {type: string, minLength: 1, maxLength: 100, examples: [Customer prefixes], description: Human-readable display name.} examples: default: {value: {name: Customer prefixes}} responses: '201': description: Created content: application/json: schema: {$ref: '#/components/schemas/GeofeedList'} '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthenticated'} '403': {$ref: '#/components/responses/Forbidden'} '409': {$ref: '#/components/responses/Conflict'} /api/v1/geofeeds/{id}: parameters: - {$ref: '#/components/parameters/OrganizationId'} - {$ref: '#/components/parameters/ResourceId'} get: tags: [Geofeeds] summary: Get a geofeed and its records description: Returns feed metadata and all current RFC 8805 records. Requires `geofeeds:read`. operationId: getGeofeed x-required-scope: geofeeds:read responses: '200': description: Geofeed content: application/json: schema: type: object required: [geofeed] properties: geofeed: {$ref: '#/components/schemas/GeofeedWithRecords'} '401': {$ref: '#/components/responses/Unauthenticated'} '403': {$ref: '#/components/responses/Forbidden'} '404': {$ref: '#/components/responses/NotFound'} patch: tags: [Geofeeds] summary: Rename a geofeed description: Changes only the display name; the public URL remains valid. Requires `geofeeds:write`. operationId: updateGeofeed x-required-scope: geofeeds:write requestBody: required: true content: application/json: schema: type: object required: [name] properties: name: {type: string, minLength: 1, maxLength: 100, examples: [Production geofeed], description: New display name.} examples: rename: {value: {name: Production geofeed}} responses: '200': description: Updated content: application/json: schema: {$ref: '#/components/schemas/StatusResponse'} '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthenticated'} '403': {$ref: '#/components/responses/Forbidden'} '404': {$ref: '#/components/responses/NotFound'} delete: tags: [Geofeeds] summary: Delete a geofeed description: Permanently deletes the feed, its records and public CSV URL. Requires `geofeeds:write`. operationId: deleteGeofeed x-required-scope: geofeeds:write responses: '200': description: Deleted content: application/json: schema: {$ref: '#/components/schemas/StatusResponse'} '401': {$ref: '#/components/responses/Unauthenticated'} '403': {$ref: '#/components/responses/Forbidden'} '404': {$ref: '#/components/responses/NotFound'} /api/v1/geofeeds/{id}/records: parameters: - {$ref: '#/components/parameters/OrganizationId'} - {$ref: '#/components/parameters/ResourceId'} put: tags: [Geofeeds] summary: Atomically replace all RFC 8805 records description: Validates the complete array and replaces all records in one transaction. No partial update is committed. Requires `geofeeds:write`. operationId: replaceGeofeedRecords x-required-scope: geofeeds:write requestBody: required: true content: application/json: schema: type: object required: [records] properties: records: type: array maxItems: 1000 items: {$ref: '#/components/schemas/GeofeedRecordInput'} examples: frankfurt: value: records: - {prefix: 203.0.113.0/24, country_code: DE, region_code: DE-HE, city: Frankfurt am Main, postal_code: '60311'} responses: '200': description: Saved content: application/json: schema: {$ref: '#/components/schemas/StatusResponse'} '400': {$ref: '#/components/responses/BadRequest'} '401': {$ref: '#/components/responses/Unauthenticated'} '403': {$ref: '#/components/responses/Forbidden'} '404': {$ref: '#/components/responses/NotFound'} /api/v1/geofeeds/{id}/rotate: parameters: - {$ref: '#/components/parameters/OrganizationId'} - {$ref: '#/components/parameters/ResourceId'} post: tags: [Geofeeds] summary: Rotate the unguessable public CSV URL description: Invalidates the previous URL immediately and issues a new public slug. Requires `geofeeds:write`. operationId: rotateGeofeedUrl x-required-scope: geofeeds:write responses: '200': description: Rotated content: application/json: schema: {$ref: '#/components/schemas/GeofeedList'} '401': {$ref: '#/components/responses/Unauthenticated'} '403': {$ref: '#/components/responses/Forbidden'} '404': {$ref: '#/components/responses/NotFound'} components: securitySchemes: apiKey: type: http scheme: bearer bearerFormat: as213905_… description: API key from Panel → Settings. Query-string keys are rejected. parameters: ResourceId: name: id in: path required: true description: UUID returned by the corresponding list endpoint. example: 38fce115-83e6-4122-a65a-9c70c9187794 schema: {type: string, format: uuid} OrganizationId: name: X-Organization-ID in: header required: true description: Organisation UUID. An API key can only address its own organisation. example: 9f44e0e7-13d4-4af3-bc6e-76b91c93b752 schema: {type: string, format: uuid} Days: name: days in: query description: Last N days; mutually exclusive with from/to. example: 7 schema: {type: integer, minimum: 1, maximum: 366, default: 7} From: name: from in: query description: First UTC calendar date in an explicit interval. Use together with `to`; mutually exclusive with `days`. example: '2026-08-01' schema: {type: string, format: date} To: name: to in: query description: Inclusive whole-day end. example: '2026-08-07' schema: {type: string, format: date} responses: BadRequest: description: Invalid request content: application/json: schema: {$ref: '#/components/schemas/APIError'} examples: invalid_parameter: {value: {error: invalid_parameter, message: "days cannot be combined with from/to"}} Unauthenticated: description: Missing, malformed, expired, revoked or unknown API key content: application/json: schema: {$ref: '#/components/schemas/APIError'} examples: missing: {value: {error: api_key_required, message: "Authorization header is required"}} invalid: {value: {error: api_key_invalid, message: "The API key is invalid or expired"}} Forbidden: description: Missing scope or disallowed source address content: application/json: schema: {$ref: '#/components/schemas/APIError'} examples: scope: {value: {error: scope_required, message: "This endpoint requires traffic:read", required_scope: traffic:read}} address: {value: {error: address_not_allowed, message: "The request source is not allowed"}} NotFound: description: Resource not found content: application/json: schema: {$ref: '#/components/schemas/APIError'} examples: resource: {value: {error: not_found, message: "Resource not found"}} Conflict: description: Resource limit or state conflict content: application/json: schema: {$ref: '#/components/schemas/APIError'} examples: limit: {value: {error: geofeed_limit_reached, message: "An organisation can have at most two geofeeds"}} RateLimited: description: Rate limit exceeded headers: Retry-After: description: Seconds before another attempt should be made. schema: {type: integer, minimum: 1, examples: [30]} content: application/json: schema: {$ref: '#/components/schemas/APIError'} examples: limited: {value: {error: rate_limited, message: "Too many requests", retry_after: 30}} schemas: APIError: type: object required: [error] properties: error: {type: string, examples: [scope_required]} message: {type: string} required_scope: {type: string} retry_after: {type: integer} StatusResponse: type: object required: [status] properties: {status: {type: string}} OrganizationResponse: type: object required: [organization] properties: {organization: {$ref: '#/components/schemas/Organization'}} Organization: type: object required: [id, asn, name, status, commit_kbps] properties: id: {type: string, format: uuid} asn: {type: integer, minimum: 1, maximum: 4294967295} name: {type: string} status: {type: string} commit_kbps: {type: integer, format: int64} examples: - {id: 9f44e0e7-13d4-4af3-bc6e-76b91c93b752, asn: 213905, name: Example Networks GmbH, status: active, commit_kbps: 1000000} Invoice: type: object required: [id, number, status, currency, total_cents] properties: id: {type: string, format: uuid} number: {type: string} status: {type: string, enum: [draft, issued, paid, overdue, cancelled]} currency: {type: string, minLength: 3, maxLength: 3} total_cents: {type: integer, format: int64} period_from: {type: [string, 'null'], format: date} period_to: {type: [string, 'null'], format: date} issued_at: {type: [string, 'null'], format: date-time} due_at: {type: [string, 'null'], format: date-time} examples: - {id: c089452d-9127-48b6-a1b8-407f9691a658, number: INV-2026-0042, status: paid, currency: EUR, total_cents: 900, period_from: '2026-08-01', period_to: '2026-08-31', issued_at: '2026-08-01T09:00:00Z', due_at: '2026-08-08T09:00:00Z'} Service: type: object required: [id, kind, label, status, monthly_cents] properties: id: {type: string, format: uuid} kind: {type: string} label: {type: string} status: {type: string, enum: [awaiting_payment, provisioning, active, suspended, terminating, terminated]} monthly_cents: {type: integer, format: int64} next_due_on: {type: [string, 'null'], format: date} controllable: {type: boolean} location: {type: string} configuration: {type: object, additionalProperties: true} examples: - {id: 38fce115-83e6-4122-a65a-9c70c9187794, kind: bgp_vm, label: BGP Small, status: active, monthly_cents: 900, next_due_on: '2026-09-01', controllable: true, location: fra, configuration: {vcpu: 2, memory_gib: 4, disk_gib: 50}} MetricPoint: type: object required: [t] properties: t: {type: string, format: date-time} v: {type: number} examples: - {t: '2026-08-14T14:00:00Z', v: 42.7} MetricSeries: type: object required: [metric, target, range, points] properties: metric: {type: string} target: {type: string} range: {type: string} points: {type: array, items: {$ref: '#/components/schemas/MetricPoint'}} examples: - {metric: Network transmit, target: net_tx, range: 6h, points: [{t: '2026-08-14T14:00:00Z', v: 1250000}, {t: '2026-08-14T14:05:00Z', v: 1310000}]} Period: type: object required: [from, to, label] properties: from: {type: string, format: date-time} to: {type: string, format: date-time} label: {type: string} examples: - {from: '2026-08-01T00:00:00Z', to: '2026-08-08T00:00:00Z', label: 1–7 August 2026} Traffic: type: object required: [p95_kbps, total_bytes, total_gb, period] properties: p95_kbps: {type: integer, format: int64} commit_kbps: {type: integer, format: int64} over_commit: {type: boolean} total_bytes: {type: integer, format: int64} total_gb: {type: number} period: {$ref: '#/components/schemas/Period'} examples: - {p95_kbps: 428500, commit_kbps: 1000000, over_commit: false, total_bytes: 482901204887, total_gb: 449.74, period: {from: '2026-08-01T00:00:00Z', to: '2026-08-08T00:00:00Z', label: 1–7 August 2026}} Tunnel: type: object required: [id, status, location, peer_endpoint] properties: id: {type: string, format: uuid} status: {type: string, enum: [provisioning, configuring, awaiting_peer, active, error, suspended, removed]} label: {type: string} location: {type: string, examples: [fra]} peer_endpoint: {type: string, format: ip} ifname: {type: string} inner_v4: {type: string} inner_v6: {type: string} announce: {type: boolean} examples: - {id: a5247bdd-e2d0-4c25-a1e6-c84e290cbfa1, status: active, label: fra-primary, location: fra, peer_endpoint: 203.0.113.10, ifname: gre-customer-1, inner_v4: 169.254.10.2/30, inner_v6: 'fe80::2/64', announce: true} CreateTunnelRequest: type: object required: [peer_endpoint, location] properties: peer_endpoint: {type: string, format: ip, examples: [203.0.113.10], description: Public remote GRE endpoint.} location: {type: string, enum: [fra], examples: [fra], description: Service location; currently Frankfurt only.} label: {type: string, maxLength: 100, examples: [fra-primary], description: Optional customer-visible name.} announce: {type: boolean, default: false, examples: [true], description: Request route announcement after configuration.} examples: - {peer_endpoint: 203.0.113.10, location: fra, label: fra-primary, announce: true} TunnelTraffic: allOf: - {$ref: '#/components/schemas/Traffic'} - type: object required: [tunnel] properties: {tunnel: {$ref: '#/components/schemas/Tunnel'}} Geofeed: type: object required: [id, name, slug, record_count, created_at, updated_at] properties: id: {type: string, format: uuid} name: {type: string} slug: {type: string} record_count: {type: integer, minimum: 0} created_at: {type: string, format: date-time} updated_at: {type: string, format: date-time} examples: - {id: 661e65fd-a498-4418-af2f-dbc8e4b24535, name: Production geofeed, slug: geo-Q4cW8sR1.csv, record_count: 1, created_at: '2026-08-14T10:00:00Z', updated_at: '2026-08-14T14:00:00Z'} GeofeedRecordInput: type: object required: [prefix, country_code] properties: prefix: {type: string, examples: [203.0.113.0/24]} country_code: {type: string, pattern: '^[A-Z]{2}$', examples: [DE]} region_code: {type: string, maxLength: 128, examples: [DE-HE]} city: {type: string, maxLength: 128, examples: [Frankfurt am Main]} postal_code: {type: string, maxLength: 128, examples: ['60311']} examples: - {prefix: 203.0.113.0/24, country_code: DE, region_code: DE-HE, city: Frankfurt am Main, postal_code: '60311'} GeofeedRecord: allOf: - {$ref: '#/components/schemas/GeofeedRecordInput'} - type: object properties: {id: {type: string, format: uuid}} GeofeedWithRecords: allOf: - {$ref: '#/components/schemas/Geofeed'} - type: object required: [records] properties: records: {type: array, items: {$ref: '#/components/schemas/GeofeedRecord'}} GeofeedList: type: object required: [geofeeds, limit] properties: geofeeds: {type: array, items: {$ref: '#/components/schemas/Geofeed'}} limit: {type: integer, const: 2} examples: - {geofeeds: [{id: 661e65fd-a498-4418-af2f-dbc8e4b24535, name: Production geofeed, slug: geo-Q4cW8sR1.csv, record_count: 1, created_at: '2026-08-14T10:00:00Z', updated_at: '2026-08-14T14:00:00Z'}], limit: 2}