612 lines
21 KiB
YAML
612 lines
21 KiB
YAML
openapi: 3.1.0
|
|
info:
|
|
title: AS213905 API
|
|
version: 1.1.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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
operationId: getServiceMetrics
|
|
x-required-scope: services:read
|
|
parameters:
|
|
- name: target
|
|
in: query
|
|
schema:
|
|
type: string
|
|
enum: [cpu_load, mem_usage_percent, net_rx, net_tx, iops, df.root.used]
|
|
default: cpu_load
|
|
- name: range
|
|
in: query
|
|
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
|
|
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]}
|
|
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
|
|
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
|
|
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
|
|
operationId: createTunnel
|
|
x-required-scope: tunnels:write
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema: {$ref: '#/components/schemas/CreateTunnelRequest'}
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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}
|
|
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
|
|
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
|
|
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}
|
|
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
|
|
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
|
|
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'}
|
|
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
|
|
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
|
|
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.
|
|
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}
|
|
From:
|
|
name: from
|
|
in: query
|
|
schema: {type: string, format: date}
|
|
To:
|
|
name: to
|
|
in: query
|
|
description: Inclusive whole-day end.
|
|
schema: {type: string, format: date}
|
|
responses:
|
|
BadRequest:
|
|
description: Invalid request
|
|
content: {application/json: {schema: {$ref: '#/components/schemas/APIError'}}}
|
|
Unauthenticated:
|
|
description: Missing, malformed, expired, revoked or unknown API key
|
|
content: {application/json: {schema: {$ref: '#/components/schemas/APIError'}}}
|
|
Forbidden:
|
|
description: Missing scope or disallowed source address
|
|
content: {application/json: {schema: {$ref: '#/components/schemas/APIError'}}}
|
|
NotFound:
|
|
description: Resource not found
|
|
content: {application/json: {schema: {$ref: '#/components/schemas/APIError'}}}
|
|
Conflict:
|
|
description: Resource limit or state conflict
|
|
content: {application/json: {schema: {$ref: '#/components/schemas/APIError'}}}
|
|
RateLimited:
|
|
description: Rate limit exceeded
|
|
headers:
|
|
Retry-After: {schema: {type: integer}}
|
|
content: {application/json: {schema: {$ref: '#/components/schemas/APIError'}}}
|
|
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}
|
|
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}
|
|
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}
|
|
MetricPoint:
|
|
type: object
|
|
required: [t]
|
|
properties:
|
|
t: {type: string, format: date-time}
|
|
v: {type: number}
|
|
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'}}
|
|
Period:
|
|
type: object
|
|
required: [from, to, label]
|
|
properties:
|
|
from: {type: string, format: date-time}
|
|
to: {type: string, format: date-time}
|
|
label: {type: string}
|
|
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'}
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|