Expanse API
Sign up
Reseller API

Authentication

Dedicated keys, independent scopes, rate limits and IP restrictions.

Authentication and scopes

Send Authorization: Bearer rk_... on all operations except openapi.json. Write does not imply read. Foreign resources return not_found.

ScopeOperations
reseller.vms.readAccount, limits, catalog, locations, VM reads, stats, rDNS/rescue/backups GET, webhooks GET
reseller.vms.writeVM mutations, initial-password GET, rDNS PUT, rescue/backup/webhook mutations
reseller.usage.readUsage and spend

Each key allows 120 requests per 60 seconds. Webhook mutations additionally share 10 per 60 seconds per account. Limiters fail closed. Honor Retry-After on 429; vm_busy returns Retry-After: 30.

Create uses JSON idempotencyKey, NOT an Idempotency-Key header. Reuse within the account returns existing VM (200); new create is 201. Persist one key per VM/order and retry same payload/key after transport failure. No general idempotency facility on other mutations.

Actions return 202 {data:{accepted:true,taskId:123}}. taskId can be null; no public task lookup exists. Poll VM detail, rescue GET or backups GET as appropriate. Accepted is not completed. Legacy jobId, vm and credential fields remain. Pending DELETE retains VM fields with accepted/taskId. Create stays 201/200; reset-password/rDNS PUT stay 200.

Statuses: creating, building, active, failed, suspended, delete_requested, deleted. Power/buildState are nullable provider strings. RAM is MiB, disk GiB, dates UTC ISO 8601; backup timestamps Unix seconds. Usage bytes and spend micros/cents are decimal strings; stats counters numbers or null. One USD is 1000000 micros. All authenticated responses have Cache-Control: no-store; spec alone has public, max-age=300.

Optional key IP allowlist

Create a key with allowedIps: ["203.0.113.10", "2001:db8::/48"] in the reseller dashboard or admin reseller key form. Enter one address or network per line. Empty lists allow any IP, including for keys created before this feature. Lists accept at most 20 entries, deduplicate, and store canonical addresses. IPv4 prefixes must be /8–/32; IPv6 prefixes /48–/128. CIDRs with host bits set are rejected with the correct network form. Zone IDs and malformed entries are rejected. IPv4-mapped IPv6 callers match IPv4 entries.

Session-authenticated owners/admins can update their own key with PATCH /api/teams/{teamId}/reseller/keys/{keyId} and platform admins can use PATCH /api/admin/resellers/{teamId}/keys/{keyId}. Body: { "allowedIps": [] } clears restrictions; an omitted field preserves them; null is invalid. These management endpoints are session-only and are not restricted by key IP lists, so an owner can recover access from any IP. Cross-account keys return 404. Revoked keys return 409 key_revoked; PATCH never restores them.

Authentication checks the secret/revocation/account, then per-key rate limit, then scope, then IP, before updating last-use time. Restrictions and revocations are read afresh for every request. A denied IP still consumes the rate limit. Missing client IPs and malformed stored lists fail closed. IP denials return 403 ip_not_allowed with "This API key is not allowed from this IP address." They do not disclose the allowlist or refresh last-use time. Input validation returns 400 invalid_request with the bad entry and a correction when applicable.

Use the public egress address seen by the panel when configuring the allowlist.