BLAZINGFAST — INTEGRATION QUICKSTART (SDK 0.1.17)
Current SDK archives include reviewed response models. Check GET /v1/version
and GET /v1/capabilities for deployed availability; source support and provider
qualification are separate. Public package registries are separate releases.

BASE URL AND AUTHENTICATION
Use https://api.staging.apps.blazingfast.io for staging, or
https://api.blazingfast.io for production. Configure the origin, not /v1.
SDKs support signed API keys or bearer authentication. The cURL examples below
use BF_API_BEARER: the complete issued bearer token, not a signed key prefix.
Keep credentials in your private environment/secret manager, not source files.
Use a staging-only key and disposable resources for purchase/lifecycle tests.

DISCOVER -> QUOTE -> DEPLOY -> WAIT
1. GET /v1/regions
2. GET /v1/products/nl/list/vps
3. GET /v1/products/nl/vps/2G/options
4. POST /v1/deploy/nl/vps/2G/quote
5. POST /v1/deploy/nl/vps/2G (one order AND full wallet payment)
6. GET /v1/operations/{operation_id}

Use returned regions, exact SKUs, OS codes, owned SSH-key IDs and add-ons. Do not
guess availability. Domain registration has its existing domain-quote workflow;
the one-call deploy route covers vps, dedicated, tcp-proxy, waf and license only.
Public prices are exact decimal strings. The quote uses account currency and
returns subtotal, tax, total and a base/add-ons/setup/tax/total breakdown.
A quote never reserves stock or funds. Deployment revalidates everything.
Existing dedicated manual approval and license availability gates still apply.

Quote (one line; replace the example OS/plan with discovered choices):
curl --fail-with-body -H "Authorization: Bearer $BF_API_BEARER" -H 'Content-Type: application/json' "$BF_API_BASE_URL/v1/deploy/nl/vps/2G/quote" --data '{"billing_cycle":"monthly","hostname":"test","os":"debian-13","max_total":"25.00"}'

Deploy (set BF_REQUEST_KEY to a new stable UUID for ONE intended purchase):
curl --fail-with-body -H "Authorization: Bearer $BF_API_BEARER" -H 'Content-Type: application/json' -H "Idempotency-Key: $BF_REQUEST_KEY" "$BF_API_BASE_URL/v1/deploy/nl/vps/2G" --data '{"billing_cycle":"monthly","hostname":"example-vps","os":"debian-13","max_total":"25.00","external_id":"example-vps-1","labels":{"environment":"test"}}'

Expected successful deployment: ok=true, status=paid, fullyPaid=true,
orderId/invoiceId, total/currency, serviceIds, provisioning=queued, operation_id
and operation_url. Paid/queued is NOT ready. Stop on validation/insufficient
balance; this route never charges a card, tops up or partially pays.
max_total is a tax/setup/add-on-inclusive spending ceiling, never a price override.
Hostname accepts a simple label or FQDN. ssh_key_id must be owned by your account.
Dedicated: connectivity_gbps/additional_ips map to authoritative priced add-ons;
OS accepts an exact discovered code or numeric profile ID. TCP: backend_ip and
ports=[{"listen":80,"backend":8080}]. License: license_ip or parent_service_id.
WAF: domain. Never mix these simple fields with the legacy billing/userConfig body.

WAITING
operation status: pending, running, succeeded, failed, cancelled or unknown.
unknown means unconfirmed, not success. Progress/stage are safe optional values.
Only succeeded confirms completion. A wait timeout never cancels or resubmits.
The deployment operation currently checks service readiness, not immutable
historical install events. Subsequent lifecycle changes can affect its result.
Opaque operation handles are customer-bound and may invalidate on key rotation.
Keep original service/action/task IDs for later diagnostics.

Python: client.deploy_product(region, product, sku, config, idempotency_key=key)
        client.product_variant_quote(region, product, sku, config)
        client.wait_until_ready(result, timeout=300)
Node.js: client.deploy_product(region, product, sku, config, {idempotency_key:key})
         await client.wait_until_ready(result, {timeout:300})
PHP: $client->deploy_product($region,$product,$sku,new DeploymentConfig(...),$key)
     $client->wait_until_ready($result,timeout:300)
Go: client.DeployProduct(region,product,sku,bfapi.DeploymentConfig{...},key)
    client.WaitForOperation(ctx,id,2*time.Second); set ctx deadline as needed.

CLI (same flags for quote and purchase; Windows/macOS/Linux):
bf deploy nl vps 2G --hostname test --os debian-13 --max-total 25.00 --quote
bf deploy nl vps 2G --hostname test --os debian-13 --max-total 25.00 --request-key UUID --wait
bf operations get OPERATION_ID
bf operations wait OPERATION_ID --wait-timeout 300
bf service list --statuses active,suspended --external-id example-vps-1 --all-batches

Renew the current service plan and billing cycle with wallet funds in ONE call:
curl --fail-with-body -X POST -H "Authorization: Bearer $BF_API_BEARER" -H "Idempotency-Key: $BF_REQUEST_KEY" "$BF_API_BASE_URL/v1/services/$SERVICE_ID/renewal"
Optional JSON body: {"max_total":"10.00"} caps the outstanding amount, not the price.
No unit/count, cycle change, card payment, top-up or separate invoice-pay call.
An eligible unpaid renewal invoice is reused. Insufficient credit commits no
debit or new invoice; existing invoices remain. Repeat the SAME key and body
after an interrupted response; a new key can buy another cycle. Domains can
return renewal_pending=true: check service details for the latest confirmation.
Example success (fictional): {"ok":true,"service_id":"00000000-0000-4000-8000-000000000001","invoice_id":"00000000-0000-4000-8000-000000000002","invoice_number":"INV-EXAMPLE-001","status":"paid","invoice_status":"paid","paid_now":"10.00","currency":"EUR","billing":{"unit":"MONTH","count":1},"paid_through":"2026-12-01T00:00:00.000Z","renewal_pending":false}
CLI: bf service renew $SERVICE_ID --idempotency-key $BF_REQUEST_KEY --max-total 10.00
SDK: Python/Node service_renewal; PHP service_renewal; Go ServiceRenewal.
MCP: renew_service_from_balance (hosted), bf_service_renewal (local mutation opt-in).
Both billing.services.write and billing.invoice.pay are required. Connection
Full/Approval/Read-only policies remain unchanged. These wrappers are source
changes until the next deployment/release; see API-MAP.txt for live versions.
JSON is the CLI default; --pretty is an optional global switch.
Hosted MCP: quote_product_variant, deploy_product, get_operation. New deploy
uses the saved connection policy: Full access runs permitted actions directly,
Approval mode requests review, Read only blocks writes. No policy bypass exists.
Local MCP mutations remain opt-in. Credentials are separate privileged reads,
never included in service lists or operation status. Do not send passwords to AI.

FILTERS AND REFERENCES
Service filtering happens BEFORE pagination. Default statuses are active and
suspended; runtime power status is independent. /v1/services defaults to limit=50,
max=100. Pagination is endpoint-specific: follow each endpoint's documented
page/limit, cursor/next_cursor or offset/next_offset model and its size bounds.
limit is canonical; page_size/pageSize are deprecated HTTP compatibility aliases
only where documented. Native SDK option names can differ.
group_by=type groups only the current page. /v1/services/grouped is an unpaginated
complete inventory under its documented status/type filters; its compatibility
default is active only.
Python/PHP iter_services, Node async iter_services and Go ForEachService handle
batches while retaining all filters. Legacy pagination metadata remains compatible.
external_id and labels are customer-owned metadata, not unique purchase keys.
Use Idempotency-Key to deduplicate payments, not external_id.
PATCH /v1/services/{id}/metadata replaces metadata; omitted fields are cleared.
GET /v1/services?external_id=... or ?label=environment=test filters exact matches.
GET /v1/billing/orders?external_id=... finds matching owned order items.
Metadata is limited to 20 simple labels, 120-character values/references; do not
store secrets. Hosted list_services supports external_id and label filtering.

ERRORS AND RETRIES
Read the canonical error.code, error.message, error.request_id and error.field_errors:
{"ok":false,"error":{"code":"scope_denied","message":"Required scope is missing","request_id":"...","field_errors":[]}}
Flat code/message/request_id/field_errors fields are deprecated legacy compatibility
fields. retryable, outcome and retry_after_seconds are optional recovery metadata.
An interrupted write/5xx can mean outcome=unknown. Inspect the returned operation
or retry the IDENTICAL request with its ORIGINAL key. Do not use a new key.
SDKs may retry GET/HEAD 429/502/503/504 with fresh signatures and Retry-After,
bounded by the request deadline. SDKs never automatically retry writes.

SIGNED CUSTOMER WEBHOOKS (FEATURE-GATED)
GET/POST /v1/webhooks; PATCH/DELETE /v1/webhooks/{id}
GET /v1/webhooks/{id}/deliveries?limit=50&offset=0
POST /v1/webhooks/{id}/deliveries/{delivery_id}/retry (failed deliveries only)
Creation requires webhooks.write and a stable Idempotency-Key. GETs require
webhooks.read. Up to 5 endpoints/account; HTTPS on 443, public IPv4 only.
Events: invoice.paid, service.ready, service.suspended, service.terminated,
operation.failed. The last currently covers Billing provisioning failures only,
not every direct power/reinstall task. No historical events are backfilled.
Creation returns a private signing_secret; list/history never return it. A replay
of that creation with the same key/body returns the same endpoint and secret.
Verify bf-signature: t=UNIX_SECONDS,v1=HEX_HMAC_SHA256.
HMAC input = timestamp + "." + exact raw JSON bytes, using signing_secret.
Use constant-time comparison, reject timestamps outside your configured tolerance
(for example five minutes), and durably deduplicate bf-event-id before processing.
Return 2xx after durably accepting the event. Delivery is at least once, unordered;
re-read owned resources when needed. Up to 12 attempts with backoff/jitter,
maximum 6 hours between attempts; no redirects or response-body collection.
Delivered history: 30 days; failed: 90 days; paused pending events remain until
resumed or endpoint deletion. Delete removes that endpoint's delivery history.
Manage signing secrets privately through API/CLI/SDK, not hosted AI tools.

TYPED CONTRACTS AND COMPATIBILITY
OpenAPI is generated from actual request validators and additive response schemas.
The new workflow has Python TypedDict, Node TypeScript declarations, Go structs
and a PHP DeploymentConfig DTO. Older Node wrappers retain compatibility typings;
not every legacy method has a fully typed response yet.
SDK download 0.1.9 is prepared with checksums; existing archives stay immutable.
API-MAP.csv distinguishes source wrapper coverage from actual live test evidence.
This release's tests used local/disposable fixtures, not production purchases.
READABLE DOMAIN AND TEMPLATE SELECTORS (source; release required)
---------------------------------------------------------------
GET /v1/domains/example.com
GET /v1/domains/example.com/dnssec
GET /v1/dns/templates/cpanel2
GET /v1/dns/zones/example.com/records/config?name=www
Existing UUID routes remain valid. Registered-domain actions also accept names.
Names are resolved within your account only. Template lookup checks your own
templates first; public system defaults are a fallback, never other accounts.
Only case-insensitive duplicates within the selected scope require a UUID.
Templates can be owned or public system templates;
changing/deleting a template still requires its UUID. URL-encode spaces/Unicode.
An owned DNS zone does not need a registered-domain service for Website Protection.
The new GET is read-only; the legacy proxy-config/show POST can ensure registration.

CLI: bf domain show example.com
CLI: bf dns template show cpanel2
CLI: bf dns config example.com --name www
Python: client.domain_get('example.com'); client.dns_record_config('example.com', name='www')
Node: await client.domain_get('example.com'); await client.dns_record_config('example.com', {name:'www'})
PHP: $client->domain_get('example.com'); $client->dns_record_config('example.com', 'www')
Go: client.DomainGet("example.com"); client.DNSRecordConfig("example.com", "www")
Hosted MCP: get_domain(service_id="example.com"), get_dns_template(template_id="cpanel2"),
get_dns_record_config(zone="example.com", record="www"). Connection policy is unchanged.

BACKUP / SNAPSHOT RETRIES (SDK 0.1.10)
-----------------------------------
Discover the owned service and provider eligibility before an action. Generate
one stable retry key per intended write and save it before submitting.
Python: client.vps_backup_run(service_id, idempotency_key=retry_key)
Node: await client.vps_backup_run(serviceId, {idempotency_key: retryKey})
PHP: $client->vps_backup_run($serviceId, $retryKey)
Go: client.VPSBackupRunTyped(serviceID, retryKey)
CLI: bf vps backup run SERVICE_ID --request-key RETRY_KEY
Snapshot create/rollback/delete and backup restore/delete/cancel/schedule writes
also accept keys. Their CLI commands require --request-key.
When enabled, durable_writes capability means keyed write identities survive
cache loss. Responses are retained for 24 hours; an older retry can return
idempotency_outcome_unknown instead of executing again. Inspect operation/state
after uncertainty. Never create a new retry key to recover an unknown outcome.

LIFECYCLE WRITE RETRIES (SDK 0.1.12)
----------------------------------
VPS start/stop/reboot/reinstall/password reset and service cancellation now
accept stable keys in Python, Node.js, PHP and Go. The wire request and old SDK
calls stay compatible. Keys use 8–128 letters/digits/dots/underscores/colons/hyphens.

CLI: run without a key to generate, save and print one before submitting:
  bf vps start SERVICE_UUID
Or supply your own stable key:
  bf vps start SERVICE_UUID --request-key external-start-0001
  bf vps reinstall SERVICE_UUID ubuntu-24.04 --request-key external-reinstall-0001
  bf service action cancel SERVICE_UUID --request-key external-cancel-0001
The alias --idempotency-key is also accepted. Supported CLI mutation commands generate
a UUID when the flag is omitted. It is saved locally before the request and
printed to stderr; JSON stdout remains unchanged. Reuse the printed key and
same arguments for a retry. Repeating a command without a key creates a new
action identity. Key records contain no credentials, request body or response.
Linux/macOS: $XDG_STATE_HOME/bf-api/requests (default ~/.local/state/bf-api/requests).
Windows: %LOCALAPPDATA%/bf-api/requests. Override: BF_API_REQUEST_KEY_DIR.
A storage error stops the command before dispatch; an explicit key avoids
local storage. Read-only operations and previews do not generate keys. Native SDK methods keep caller-controlled optional/required keys.
Discover supported OS codes with bf vps reinstall-os SERVICE_UUID first.

Native SDK examples (use the same original key and payload for an explicit retry):
  Python: client.vps_start(service_id, idempotency_key="external-start-0001")
  Node:   await client.vps_start(serviceId, {idempotency_key:"external-start-0001"})
  PHP:    $client->vps_start($serviceId, "external-start-0001");
  Go:     client.VPSStart(serviceID, "external-start-0001")

An interrupted write is never automatically resubmitted. Inspect service state,
its returned action ID with bf vps action-status SERVICE_UUID ACTION_ID, or its
operation handle with bf operations get OPERATION_ID. A pending/unknown outcome
is not proof that nothing happened. Never create a new key to retry it.
With durable_writes enabled, responses are encrypted and redacted after 24 hours;
the retained identity still blocks redispatch after response expiry. Without a
key the API retains legacy behavior. Other write methods may still need expanded
native key options; SDK 0.1.12 does not claim universal keyed-write coverage.

MACHINE-READABLE WORKFLOW (check /v1/version and /v1/capabilities)
----------------------------------------------------------------
Product purchase.draft_order and purchase.atomic_deploy advertise their own
implemented/enabled/eligible/permitted policies. Follow requirements_url or
options_url; requirements.atomic_deploy uses canonical field names and discovery
sources. Discovery does not reserve stock or authorize spending.

An optional quote returns quote.recommended_max_total in quote.currency. Obtain
authorization and carry that exact ceiling into max_total. Deployment revalidates
all costs; HTTP 409 deployment_price_limit_exceeded refuses a higher total.
quote.guaranteed=false; quote.id/pricing_revision fingerprint this priced cart,
not a price lock or a redeemable quote resource. Follow links.deploy.

Opt into a canonical write receipt with x-bf-response-format: operation only
when canonical_operation_responses is enabled. Legacy fields remain the default.
SDK deployment opts: Python/Node operation_response=true; PHP final boolean true;
Go ProductVariantDeploy(..., true). Saved retries retain their initial format.
Polling accepts operation.id or operation_id. A terminal successful receipt has
result resource IDs; failure/cancellation must be reconciled with billing.
Progress/stage may be null. A service lifecycle state is not runtime power.
DNSSEC continues to use its own state response and polling endpoint.

After losing a tracked write response, reuse the ORIGINAL key for recovery:
  GET /v1/operations?idempotency_key=original-key
  bf operations recover --request-key original-key --method POST \
      --path /v1/vps/SERVICE_ID/reboot --wait
Python/Node/PHP: operation_recover; Go: OperationRecover.
Hosted MCP: recover_operation; local MCP: bf_operation_recover.
Scopes/ownership are checked again. method and path must be supplied together;
credential_id disambiguates the same key used under multiple credentials.
404 operation_not_found is not proof of no side effect. 409 requires more precise
selectors. 410 operation_expired means reconcile resources/billing, not resubmit.
Terminal receipts retain 90 days by default; pending/unknown receipts remain for
reconciliation. Lookup identities persist after expiry. Recovery applies to newly
tracked deployment/VPS/dedicated writes; check capability and endpoint coverage.
MCP approval requests and execution receipts remain separate.
