Expand API reference and repair Swagger theme

This commit is contained in:
AS213905 Engineering
2026-08-14 11:39:23 +00:00
parent 8c17dafc1f
commit dd05901571
4 changed files with 212 additions and 28 deletions
+4
View File
@@ -49,6 +49,10 @@ organisation UUID and needs `geofeeds:read` or `geofeeds:write`.
- [`openapi/openapi.yaml`](openapi/openapi.yaml) is the source of truth. - [`openapi/openapi.yaml`](openapi/openapi.yaml) is the source of truth.
- Interactive reference: <https://as213905.com/docs/api/reference> - Interactive reference: <https://as213905.com/docs/api/reference>
- Machine-readable production contract: <https://as213905.com/api/v1/openapi.json> - Machine-readable production contract: <https://as213905.com/api/v1/openapi.json>
- Getting started: <https://as213905.com/docs/api/getting-started>
- Authentication and scopes: <https://as213905.com/docs/api/authentication>
- Complete endpoint examples: <https://as213905.com/docs/api/endpoints>
- SDK guides: <https://as213905.com/docs/api/sdks>
## Development ## Development
+88 -9
View File
@@ -7,22 +7,101 @@
<title>AS213905 API reference</title> <title>AS213905 API reference</title>
<link rel="stylesheet" href="/docs/api/reference/assets/swagger-ui.css" /> <link rel="stylesheet" href="/docs/api/reference/assets/swagger-ui.css" />
<style> <style>
:root { color-scheme: dark; --bg:#201d21; --surface:#29252a; --surface-2:#332f34; --line:#4a454b; --text:#f1edf2; --muted:#b7b0b8; --brand:#ffbe5c; --link:#7cb8ff; }
html { box-sizing: border-box; overflow-y: scroll; } html { box-sizing: border-box; overflow-y: scroll; }
*, *::before, *::after { box-sizing: inherit; } *, *::before, *::after { box-sizing: inherit; }
body { margin: 0; background: #201d21; } body { margin: 0; background: var(--bg); color: var(--text); font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
.swagger-ui .topbar { background: #171518; border-bottom: 1px solid #4a454b; } .api-header { position:sticky; top:0; z-index:20; display:flex; align-items:center; justify-content:space-between; gap:20px; min-height:62px; padding:10px max(20px, calc((100vw - 1200px)/2)); border-bottom:1px solid var(--line); background:#171518f2; backdrop-filter:blur(12px); }
.swagger-ui .topbar-wrapper img { display: none; } .api-header__brand { color:var(--brand); font-size:17px; font-weight:700; text-decoration:none; }
.swagger-ui .topbar-wrapper::before { content: "AS213905 API"; color: #ffbe5c; font: 600 18px system-ui; } .api-header__links { display:flex; align-items:center; gap:8px; }
.swagger-ui .scheme-container { background: #29252a; box-shadow: none; } .api-header__links a { padding:8px 11px; border-radius:8px; color:var(--muted); font-size:13px; font-weight:600; text-decoration:none; }
@media (max-width: 640px) { .api-header__links a:hover { color:var(--text); background:var(--surface-2); }
.swagger-ui { color:var(--text); font-family:inherit; }
.swagger-ui .wrapper { max-width:1200px; padding:0 20px; }
.swagger-ui .information-container { margin:0; padding:36px 0 20px; }
.swagger-ui .info { margin:0 0 30px; }
.swagger-ui .info .title, .swagger-ui .info h1, .swagger-ui .info h2, .swagger-ui .info h3, .swagger-ui .info h4, .swagger-ui .info h5,
.swagger-ui .opblock-tag, .swagger-ui .opblock .opblock-summary-path, .swagger-ui .opblock .opblock-summary-description,
.swagger-ui .opblock-description-wrapper p, .swagger-ui .opblock-external-docs-wrapper p, .swagger-ui .opblock-title_normal p,
.swagger-ui .responses-inner h4, .swagger-ui .responses-inner h5, .swagger-ui .model-title, .swagger-ui .model, .swagger-ui .model-box,
.swagger-ui table thead tr th, .swagger-ui table thead tr td, .swagger-ui .parameter__name, .swagger-ui .parameter__type,
.swagger-ui .response-col_status, .swagger-ui .response-col_description, .swagger-ui .tab li, .swagger-ui label,
.swagger-ui .servers-title, .swagger-ui .servers > label, .swagger-ui .scheme-container .schemes > label { color:var(--text); }
.swagger-ui .info p, .swagger-ui .info li, .swagger-ui .info table, .swagger-ui .markdown p, .swagger-ui .markdown li,
.swagger-ui .parameter__deprecated, .swagger-ui .parameter__in, .swagger-ui .renderedMarkdown p { color:var(--muted); }
.swagger-ui a, .swagger-ui .info a { color:var(--link); }
.swagger-ui .info .title small { top:-3px; }
.swagger-ui .scheme-container { margin:0 0 28px; padding:20px 0; background:var(--surface); box-shadow:0 0 0 100vmax var(--surface); clip-path:inset(0 -100vmax); }
.swagger-ui .auth-wrapper .authorize { border-color:#42d7a4; color:#42d7a4; background:transparent; }
.swagger-ui select, .swagger-ui input[type=text], .swagger-ui input[type=email], .swagger-ui input[type=password], .swagger-ui textarea {
min-height:36px; border:1px solid #6a636b; border-radius:6px; color:#171518; background:#fff; font-family:inherit; box-shadow:none;
}
.swagger-ui select:focus, .swagger-ui input:focus, .swagger-ui textarea:focus { outline:2px solid #7cb8ff; outline-offset:1px; }
.swagger-ui .filter .operation-filter-input { border:1px solid #6a636b; color:#171518; background:#fff; }
.swagger-ui .opblock-tag { margin:0 0 12px; border-bottom:1px solid var(--line); font-size:22px; }
.swagger-ui .opblock-tag small, .swagger-ui .opblock-tag small p { color:var(--muted); }
.swagger-ui .opblock { margin:0 0 12px; border-width:1px; border-radius:9px; background:var(--surface); box-shadow:none; overflow:hidden; }
.swagger-ui .opblock .opblock-summary { min-height:54px; border-color:color-mix(in srgb, currentColor 32%, transparent); }
.swagger-ui .opblock .opblock-summary-method { min-width:78px; border-radius:6px; text-shadow:none; }
.swagger-ui .opblock .opblock-summary-path { font-size:15px; }
.swagger-ui .opblock .opblock-summary-description { color:var(--muted); }
.swagger-ui .opblock-body { background:#252226; }
.swagger-ui .opblock-section-header { min-height:48px; padding:10px 20px; background:var(--surface-2); box-shadow:none; border-top:1px solid var(--line); border-bottom:1px solid var(--line); }
.swagger-ui .opblock-section-header h4 { color:var(--text); }
.swagger-ui .parameters-container, .swagger-ui .responses-wrapper, .swagger-ui .opblock-description-wrapper { padding:14px 20px; }
.swagger-ui table tbody tr td { padding:14px 10px; border-color:var(--line); color:var(--text); vertical-align:top; }
.swagger-ui table.parameters tbody tr { border-bottom:1px solid var(--line); }
.swagger-ui table.parameters tbody tr:last-child { border-bottom:0; }
.swagger-ui .parameters-col_name { min-width:190px; }
.swagger-ui .parameters-col_description { width:auto; color:var(--text); }
.swagger-ui .parameters-col_description p { margin:0 0 8px; color:var(--muted); }
.swagger-ui .parameter__name.required::after { color:#ff6e8a; }
.swagger-ui .parameter__extension, .swagger-ui .parameter__example { color:var(--muted); }
.swagger-ui .responses-table .response-col_status { color:var(--text); }
.swagger-ui .response-col_links { color:var(--muted); }
.swagger-ui .model-box { background:#171518; border:1px solid var(--line); border-radius:7px; }
.swagger-ui .model-toggle::after { filter:invert(1); }
.swagger-ui section.models { border:1px solid var(--line); border-radius:9px; background:var(--surface); }
.swagger-ui section.models h4 { color:var(--text); }
.swagger-ui section.models .model-container { background:#171518; }
.swagger-ui .prop-type { color:#8fc8ff; }
.swagger-ui .prop-format { color:var(--muted); }
.swagger-ui .highlight-code, .swagger-ui .microlight, .swagger-ui pre { color:#f5f1f6 !important; background:#171518 !important; text-shadow:none !important; }
.swagger-ui .copy-to-clipboard { background:#3d383e; }
.swagger-ui .btn { border-color:#777078; color:var(--text); background:transparent; }
.swagger-ui .btn:hover { background:var(--surface-2); }
.swagger-ui .btn.execute { border-color:#499ce8; color:#fff; background:#287cc8; }
.swagger-ui .loading-container .loading::after { color:var(--text); }
.swagger-ui .dialog-ux .modal-ux { border-color:var(--line); background:var(--surface); }
.swagger-ui .dialog-ux .modal-ux-header, .swagger-ui .dialog-ux .modal-ux-content { color:var(--text); border-color:var(--line); }
.swagger-ui .dialog-ux .modal-ux-header h3 { color:var(--text); }
.swagger-ui .dialog-ux .modal-ux-header .close-modal { filter:invert(1); }
@media (max-width: 700px) {
.api-header { padding:10px 12px; }
.api-header__links a:first-child { display:none; }
.swagger-ui .wrapper { padding:0 12px; } .swagger-ui .wrapper { padding:0 12px; }
.swagger-ui .opblock .opblock-summary { flex-wrap: wrap; } .swagger-ui .information-container { padding-top:24px; }
.swagger-ui .opblock-summary-path { max-width: 100%; font-size: 13px; } .swagger-ui .opblock .opblock-summary { flex-wrap:wrap; gap:6px; padding:8px; }
.swagger-ui .opblock .opblock-summary-path { flex:1 1 calc(100% - 92px); max-width:none; font-size:12px; overflow-wrap:anywhere; }
.swagger-ui .opblock .opblock-summary-description { flex:1 1 100%; padding-left:0; }
.swagger-ui .parameters-container, .swagger-ui .responses-wrapper, .swagger-ui .opblock-description-wrapper { padding:12px; }
.swagger-ui table.parameters, .swagger-ui table.parameters tbody, .swagger-ui table.parameters tr, .swagger-ui table.parameters td { display:block; width:100%; }
.swagger-ui table.parameters thead { display:none; }
.swagger-ui table.parameters td { padding:8px 0; }
.swagger-ui .parameters-col_name { min-width:0; }
} }
</style> </style>
</head> </head>
<body> <body>
<div id="swagger-ui"></div> <header class="api-header">
<a class="api-header__brand" href="/docs/api">AS213905 API</a>
<nav class="api-header__links">
<a href="/docs/api/endpoints">Guides & examples</a>
<a href="/docs/api/sdks">SDKs</a>
<a href="/api/v1/openapi.json">OpenAPI JSON</a>
</nav>
</header>
<main id="swagger-ui"></main>
<script src="/docs/api/reference/assets/swagger-ui-bundle.js"></script> <script src="/docs/api/reference/assets/swagger-ui-bundle.js"></script>
<script src="/docs/api/reference/assets/swagger-ui-standalone-preset.js"></script> <script src="/docs/api/reference/assets/swagger-ui-standalone-preset.js"></script>
<script src="/docs/api/reference/swagger-initializer.js"></script> <script src="/docs/api/reference/swagger-initializer.js"></script>
+6 -1
View File
@@ -7,7 +7,12 @@ window.addEventListener('load', () => {
filter: true, filter: true,
persistAuthorization: false, persistAuthorization: false,
tryItOutEnabled: false, tryItOutEnabled: false,
defaultModelsExpandDepth: 1,
defaultModelExpandDepth: 2,
displayOperationId: true,
showExtensions: true,
showCommonExtensions: true,
presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset], presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
layout: 'StandaloneLayout', layout: 'BaseLayout',
}); });
}); });
+113 -17
View File
@@ -1,7 +1,7 @@
openapi: 3.1.0 openapi: 3.1.0
info: info:
title: AS213905 API title: AS213905 API
version: 1.1.0 version: 1.2.0
description: | description: |
Customer automation API for organisations, invoices, services, traffic, Customer automation API for organisations, invoices, services, traffic,
tunnels and RFC 8805 geofeeds. Create a scoped key in Panel → Settings. tunnels and RFC 8805 geofeeds. Create a scoped key in Panel → Settings.
@@ -34,6 +34,7 @@ paths:
get: get:
tags: [Organisation] tags: [Organisation]
summary: Get the API key's organisation summary: Get the API key's organisation
description: Returns the single organisation permanently bound to the bearer key. Requires `organizations:read`.
operationId: getOrganization operationId: getOrganization
x-required-scope: organizations:read x-required-scope: organizations:read
responses: responses:
@@ -48,6 +49,7 @@ paths:
get: get:
tags: [Invoices] tags: [Invoices]
summary: List invoices, newest first summary: List invoices, newest first
description: Returns all billing documents for the key organisation, ordered from newest to oldest. Requires `invoices:read`.
operationId: listInvoices operationId: listInvoices
x-required-scope: invoices:read x-required-scope: invoices:read
responses: responses:
@@ -66,6 +68,7 @@ paths:
get: get:
tags: [Services] tags: [Services]
summary: List services summary: List services
description: Returns every customer service and its lifecycle state. Requires `services:read`.
operationId: listServices operationId: listServices
x-required-scope: services:read x-required-scope: services:read
responses: responses:
@@ -86,6 +89,7 @@ paths:
get: get:
tags: [Services] tags: [Services]
summary: Get one service 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 operationId: getService
x-required-scope: services:read x-required-scope: services:read
responses: responses:
@@ -107,17 +111,22 @@ paths:
get: get:
tags: [Services] tags: [Services]
summary: Read a BGP VM metric series summary: Read a BGP VM metric series
description: Reads a sampled metric series from a controllable BGP VM. Requires `services:read`.
operationId: getServiceMetrics operationId: getServiceMetrics
x-required-scope: services:read x-required-scope: services:read
parameters: parameters:
- name: target - name: target
in: query 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: schema:
type: string type: string
enum: [cpu_load, mem_usage_percent, net_rx, net_tx, iops, df.root.used] enum: [cpu_load, mem_usage_percent, net_rx, net_tx, iops, df.root.used]
default: cpu_load default: cpu_load
- name: range - name: range
in: query in: query
description: Time window ending at the current time.
example: 6h
schema: {type: string, enum: [1h, 6h, 24h, 7d], default: 24h} schema: {type: string, enum: [1h, 6h, 24h, 7d], default: 24h}
responses: responses:
'200': '200':
@@ -134,6 +143,7 @@ paths:
post: post:
tags: [Services] tags: [Services]
summary: Start, stop or restart a BGP VM summary: Start, stop or restart a BGP VM
description: Queues an asynchronous power command for a controllable BGP VM. Requires `services:write`.
operationId: powerService operationId: powerService
x-required-scope: services:write x-required-scope: services:write
requestBody: requestBody:
@@ -144,7 +154,9 @@ paths:
type: object type: object
required: [action] required: [action]
properties: properties:
action: {type: string, enum: [start, stop, restart]} action: {type: string, enum: [start, stop, restart], examples: [restart], description: Power transition to queue.}
examples:
restart: {value: {action: restart}}
responses: responses:
'202': '202':
description: Command accepted description: Command accepted
@@ -164,6 +176,7 @@ paths:
get: get:
tags: [Traffic] tags: [Traffic]
summary: Get organisation transit usage 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 operationId: getTraffic
x-required-scope: traffic:read x-required-scope: traffic:read
parameters: parameters:
@@ -183,6 +196,7 @@ paths:
get: get:
tags: [Tunnels] tags: [Tunnels]
summary: List tunnels summary: List tunnels
description: Returns all transit tunnels and their provisioning state. Requires `tunnels:read`.
operationId: listTunnels operationId: listTunnels
x-required-scope: tunnels:read x-required-scope: tunnels:read
responses: responses:
@@ -200,6 +214,7 @@ paths:
post: post:
tags: [Tunnels] tags: [Tunnels]
summary: Request a transit tunnel 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 operationId: createTunnel
x-required-scope: tunnels:write x-required-scope: tunnels:write
requestBody: requestBody:
@@ -207,6 +222,10 @@ paths:
content: content:
application/json: application/json:
schema: {$ref: '#/components/schemas/CreateTunnelRequest'} 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: responses:
'202': '202':
description: Tunnel recorded for provisioning description: Tunnel recorded for provisioning
@@ -226,6 +245,7 @@ paths:
delete: delete:
tags: [Tunnels] tags: [Tunnels]
summary: Remove a tunnel asynchronously summary: Remove a tunnel asynchronously
description: Queues teardown and returns before router configuration has completed. Requires `tunnels:write`.
operationId: deleteTunnel operationId: deleteTunnel
x-required-scope: tunnels:write x-required-scope: tunnels:write
responses: responses:
@@ -243,6 +263,7 @@ paths:
get: get:
tags: [Traffic, Tunnels] tags: [Traffic, Tunnels]
summary: Get one tunnel's usage 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 operationId: getTunnelTraffic
x-required-scope: traffic:read x-required-scope: traffic:read
parameters: parameters:
@@ -265,6 +286,7 @@ paths:
get: get:
tags: [Geofeeds] tags: [Geofeeds]
summary: List 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 operationId: listGeofeeds
x-required-scope: geofeeds:read x-required-scope: geofeeds:read
responses: responses:
@@ -278,6 +300,7 @@ paths:
post: post:
tags: [Geofeeds] tags: [Geofeeds]
summary: Create a geofeed (maximum two per organisation) summary: Create a geofeed (maximum two per organisation)
description: Creates an empty feed and an unguessable public CSV URL. Requires `geofeeds:write`.
operationId: createGeofeed operationId: createGeofeed
x-required-scope: geofeeds:write x-required-scope: geofeeds:write
requestBody: requestBody:
@@ -288,7 +311,9 @@ paths:
type: object type: object
required: [name] required: [name]
properties: properties:
name: {type: string, minLength: 1, maxLength: 100} name: {type: string, minLength: 1, maxLength: 100, examples: [Customer prefixes], description: Human-readable display name.}
examples:
default: {value: {name: Customer prefixes}}
responses: responses:
'201': '201':
description: Created description: Created
@@ -306,6 +331,7 @@ paths:
get: get:
tags: [Geofeeds] tags: [Geofeeds]
summary: Get a geofeed and its records summary: Get a geofeed and its records
description: Returns feed metadata and all current RFC 8805 records. Requires `geofeeds:read`.
operationId: getGeofeed operationId: getGeofeed
x-required-scope: geofeeds:read x-required-scope: geofeeds:read
responses: responses:
@@ -324,6 +350,7 @@ paths:
patch: patch:
tags: [Geofeeds] tags: [Geofeeds]
summary: Rename a geofeed summary: Rename a geofeed
description: Changes only the display name; the public URL remains valid. Requires `geofeeds:write`.
operationId: updateGeofeed operationId: updateGeofeed
x-required-scope: geofeeds:write x-required-scope: geofeeds:write
requestBody: requestBody:
@@ -334,7 +361,9 @@ paths:
type: object type: object
required: [name] required: [name]
properties: properties:
name: {type: string, minLength: 1, maxLength: 100} name: {type: string, minLength: 1, maxLength: 100, examples: [Production geofeed], description: New display name.}
examples:
rename: {value: {name: Production geofeed}}
responses: responses:
'200': '200':
description: Updated description: Updated
@@ -348,6 +377,7 @@ paths:
delete: delete:
tags: [Geofeeds] tags: [Geofeeds]
summary: Delete a geofeed summary: Delete a geofeed
description: Permanently deletes the feed, its records and public CSV URL. Requires `geofeeds:write`.
operationId: deleteGeofeed operationId: deleteGeofeed
x-required-scope: geofeeds:write x-required-scope: geofeeds:write
responses: responses:
@@ -366,6 +396,7 @@ paths:
put: put:
tags: [Geofeeds] tags: [Geofeeds]
summary: Atomically replace all RFC 8805 records 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 operationId: replaceGeofeedRecords
x-required-scope: geofeeds:write x-required-scope: geofeeds:write
requestBody: requestBody:
@@ -380,6 +411,11 @@ paths:
type: array type: array
maxItems: 1000 maxItems: 1000
items: {$ref: '#/components/schemas/GeofeedRecordInput'} 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: responses:
'200': '200':
description: Saved description: Saved
@@ -397,6 +433,7 @@ paths:
post: post:
tags: [Geofeeds] tags: [Geofeeds]
summary: Rotate the unguessable public CSV URL summary: Rotate the unguessable public CSV URL
description: Invalidates the previous URL immediately and issues a new public slug. Requires `geofeeds:write`.
operationId: rotateGeofeedUrl operationId: rotateGeofeedUrl
x-required-scope: geofeeds:write x-required-scope: geofeeds:write
responses: responses:
@@ -420,48 +457,83 @@ components:
name: id name: id
in: path in: path
required: true required: true
description: UUID returned by the corresponding list endpoint.
example: 38fce115-83e6-4122-a65a-9c70c9187794
schema: {type: string, format: uuid} schema: {type: string, format: uuid}
OrganizationId: OrganizationId:
name: X-Organization-ID name: X-Organization-ID
in: header in: header
required: true required: true
description: Organisation UUID. An API key can only address its own organisation. description: Organisation UUID. An API key can only address its own organisation.
example: 9f44e0e7-13d4-4af3-bc6e-76b91c93b752
schema: {type: string, format: uuid} schema: {type: string, format: uuid}
Days: Days:
name: days name: days
in: query in: query
description: Last N days; mutually exclusive with from/to. description: Last N days; mutually exclusive with from/to.
schema: {type: integer, minimum: 1, maximum: 366} example: 7
schema: {type: integer, minimum: 1, maximum: 366, default: 7}
From: From:
name: from name: from
in: query 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} schema: {type: string, format: date}
To: To:
name: to name: to
in: query in: query
description: Inclusive whole-day end. description: Inclusive whole-day end.
example: '2026-08-07'
schema: {type: string, format: date} schema: {type: string, format: date}
responses: responses:
BadRequest: BadRequest:
description: Invalid request description: Invalid request
content: {application/json: {schema: {$ref: '#/components/schemas/APIError'}}} content:
application/json:
schema: {$ref: '#/components/schemas/APIError'}
examples:
invalid_parameter: {value: {error: invalid_parameter, message: "days cannot be combined with from/to"}}
Unauthenticated: Unauthenticated:
description: Missing, malformed, expired, revoked or unknown API key description: Missing, malformed, expired, revoked or unknown API key
content: {application/json: {schema: {$ref: '#/components/schemas/APIError'}}} 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: Forbidden:
description: Missing scope or disallowed source address description: Missing scope or disallowed source address
content: {application/json: {schema: {$ref: '#/components/schemas/APIError'}}} 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: NotFound:
description: Resource not found description: Resource not found
content: {application/json: {schema: {$ref: '#/components/schemas/APIError'}}} content:
application/json:
schema: {$ref: '#/components/schemas/APIError'}
examples:
resource: {value: {error: not_found, message: "Resource not found"}}
Conflict: Conflict:
description: Resource limit or state conflict description: Resource limit or state conflict
content: {application/json: {schema: {$ref: '#/components/schemas/APIError'}}} 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: RateLimited:
description: Rate limit exceeded description: Rate limit exceeded
headers: headers:
Retry-After: {schema: {type: integer}} Retry-After:
content: {application/json: {schema: {$ref: '#/components/schemas/APIError'}}} 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: schemas:
APIError: APIError:
type: object type: object
@@ -488,6 +560,8 @@ components:
name: {type: string} name: {type: string}
status: {type: string} status: {type: string}
commit_kbps: {type: integer, format: int64} 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: Invoice:
type: object type: object
required: [id, number, status, currency, total_cents] required: [id, number, status, currency, total_cents]
@@ -501,6 +575,8 @@ components:
period_to: {type: [string, 'null'], format: date} period_to: {type: [string, 'null'], format: date}
issued_at: {type: [string, 'null'], format: date-time} issued_at: {type: [string, 'null'], format: date-time}
due_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: Service:
type: object type: object
required: [id, kind, label, status, monthly_cents] required: [id, kind, label, status, monthly_cents]
@@ -514,12 +590,16 @@ components:
controllable: {type: boolean} controllable: {type: boolean}
location: {type: string} location: {type: string}
configuration: {type: object, additionalProperties: true} 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: MetricPoint:
type: object type: object
required: [t] required: [t]
properties: properties:
t: {type: string, format: date-time} t: {type: string, format: date-time}
v: {type: number} v: {type: number}
examples:
- {t: '2026-08-14T14:00:00Z', v: 42.7}
MetricSeries: MetricSeries:
type: object type: object
required: [metric, target, range, points] required: [metric, target, range, points]
@@ -528,6 +608,8 @@ components:
target: {type: string} target: {type: string}
range: {type: string} range: {type: string}
points: {type: array, items: {$ref: '#/components/schemas/MetricPoint'}} 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: Period:
type: object type: object
required: [from, to, label] required: [from, to, label]
@@ -535,6 +617,8 @@ components:
from: {type: string, format: date-time} from: {type: string, format: date-time}
to: {type: string, format: date-time} to: {type: string, format: date-time}
label: {type: string} label: {type: string}
examples:
- {from: '2026-08-01T00:00:00Z', to: '2026-08-08T00:00:00Z', label: 17 August 2026}
Traffic: Traffic:
type: object type: object
required: [p95_kbps, total_bytes, total_gb, period] required: [p95_kbps, total_bytes, total_gb, period]
@@ -545,6 +629,8 @@ components:
total_bytes: {type: integer, format: int64} total_bytes: {type: integer, format: int64}
total_gb: {type: number} total_gb: {type: number}
period: {$ref: '#/components/schemas/Period'} 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: 17 August 2026}}
Tunnel: Tunnel:
type: object type: object
required: [id, status, location, peer_endpoint] required: [id, status, location, peer_endpoint]
@@ -558,14 +644,18 @@ components:
inner_v4: {type: string} inner_v4: {type: string}
inner_v6: {type: string} inner_v6: {type: string}
announce: {type: boolean} 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: CreateTunnelRequest:
type: object type: object
required: [peer_endpoint, location] required: [peer_endpoint, location]
properties: properties:
peer_endpoint: {type: string, format: ip} peer_endpoint: {type: string, format: ip, examples: [203.0.113.10], description: Public remote GRE endpoint.}
location: {type: string, examples: [fra]} location: {type: string, enum: [fra], examples: [fra], description: Service location; currently Frankfurt only.}
label: {type: string, maxLength: 100} label: {type: string, maxLength: 100, examples: [fra-primary], description: Optional customer-visible name.}
announce: {type: boolean, default: false} 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: TunnelTraffic:
allOf: allOf:
- {$ref: '#/components/schemas/Traffic'} - {$ref: '#/components/schemas/Traffic'}
@@ -582,6 +672,8 @@ components:
record_count: {type: integer, minimum: 0} record_count: {type: integer, minimum: 0}
created_at: {type: string, format: date-time} created_at: {type: string, format: date-time}
updated_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: GeofeedRecordInput:
type: object type: object
required: [prefix, country_code] required: [prefix, country_code]
@@ -590,7 +682,9 @@ components:
country_code: {type: string, pattern: '^[A-Z]{2}$', examples: [DE]} country_code: {type: string, pattern: '^[A-Z]{2}$', examples: [DE]}
region_code: {type: string, maxLength: 128, examples: [DE-HE]} region_code: {type: string, maxLength: 128, examples: [DE-HE]}
city: {type: string, maxLength: 128, examples: [Frankfurt am Main]} city: {type: string, maxLength: 128, examples: [Frankfurt am Main]}
postal_code: {type: string, maxLength: 128} 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: GeofeedRecord:
allOf: allOf:
- {$ref: '#/components/schemas/GeofeedRecordInput'} - {$ref: '#/components/schemas/GeofeedRecordInput'}
@@ -609,3 +703,5 @@ components:
properties: properties:
geofeeds: {type: array, items: {$ref: '#/components/schemas/Geofeed'}} geofeeds: {type: array, items: {$ref: '#/components/schemas/Geofeed'}}
limit: {type: integer, const: 2} 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}