From dd05901571d21e1b04d9b9795a8ac24907a11e77 Mon Sep 17 00:00:00 2001 From: AS213905 Engineering Date: Fri, 14 Aug 2026 11:39:23 +0000 Subject: [PATCH] Expand API reference and repair Swagger theme --- README.md | 4 ++ docs/index.html | 99 ++++++++++++++++++++++++--- docs/swagger-initializer.js | 7 +- openapi/openapi.yaml | 130 +++++++++++++++++++++++++++++++----- 4 files changed, 212 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 3184fae..9ef9f3f 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,10 @@ organisation UUID and needs `geofeeds:read` or `geofeeds:write`. - [`openapi/openapi.yaml`](openapi/openapi.yaml) is the source of truth. - Interactive reference: - Machine-readable production contract: +- Getting started: +- Authentication and scopes: +- Complete endpoint examples: +- SDK guides: ## Development diff --git a/docs/index.html b/docs/index.html index 225dd78..e400ea4 100644 --- a/docs/index.html +++ b/docs/index.html @@ -7,22 +7,101 @@ AS213905 API reference -
+
+ AS213905 API + +
+
diff --git a/docs/swagger-initializer.js b/docs/swagger-initializer.js index bbce495..d2ff7f4 100644 --- a/docs/swagger-initializer.js +++ b/docs/swagger-initializer.js @@ -7,7 +7,12 @@ window.addEventListener('load', () => { filter: true, persistAuthorization: false, tryItOutEnabled: false, + defaultModelsExpandDepth: 1, + defaultModelExpandDepth: 2, + displayOperationId: true, + showExtensions: true, + showCommonExtensions: true, presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset], - layout: 'StandaloneLayout', + layout: 'BaseLayout', }); }); diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 078b153..779122b 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -1,7 +1,7 @@ openapi: 3.1.0 info: title: AS213905 API - version: 1.1.0 + 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. @@ -34,6 +34,7 @@ paths: 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: @@ -48,6 +49,7 @@ paths: 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: @@ -66,6 +68,7 @@ paths: 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: @@ -86,6 +89,7 @@ paths: 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: @@ -107,17 +111,22 @@ paths: 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': @@ -134,6 +143,7 @@ paths: 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: @@ -144,7 +154,9 @@ paths: type: object required: [action] 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: '202': description: Command accepted @@ -164,6 +176,7 @@ paths: 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: @@ -183,6 +196,7 @@ paths: 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,6 +214,7 @@ paths: 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: @@ -207,6 +222,10 @@ paths: 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 @@ -226,6 +245,7 @@ paths: 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: @@ -243,6 +263,7 @@ paths: 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: @@ -265,6 +286,7 @@ paths: 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: @@ -278,6 +300,7 @@ paths: 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: @@ -288,7 +311,9 @@ paths: type: object required: [name] 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: '201': description: Created @@ -306,6 +331,7 @@ paths: 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: @@ -324,6 +350,7 @@ paths: 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: @@ -334,7 +361,9 @@ paths: type: object required: [name] 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: '200': description: Updated @@ -348,6 +377,7 @@ paths: 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: @@ -366,6 +396,7 @@ paths: 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: @@ -380,6 +411,11 @@ paths: 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 @@ -397,6 +433,7 @@ paths: 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: @@ -420,48 +457,83 @@ components: 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. - schema: {type: integer, minimum: 1, maximum: 366} + 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'}}} + 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'}}} + 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'}}} + 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'}}} + 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'}}} + 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: {schema: {type: integer}} - content: {application/json: {schema: {$ref: '#/components/schemas/APIError'}}} + 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 @@ -488,6 +560,8 @@ components: 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] @@ -501,6 +575,8 @@ components: 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] @@ -514,12 +590,16 @@ components: 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] @@ -528,6 +608,8 @@ components: 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] @@ -535,6 +617,8 @@ components: 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] @@ -545,6 +629,8 @@ components: 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] @@ -558,14 +644,18 @@ components: 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} - location: {type: string, examples: [fra]} - label: {type: string, maxLength: 100} - announce: {type: boolean, default: false} + 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'} @@ -582,6 +672,8 @@ components: 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] @@ -590,7 +682,9 @@ components: 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} + 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'} @@ -609,3 +703,5 @@ components: 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}