# Windscribe VPN API > Accountless, time-limited Windscribe WireGuard VPN access that software can buy on its own. Payment is x402 v2: USDC on Base, settled on-chain per request. The WireGuard public key is the only identity: no Windscribe account, email, password, session or custodial wallet. Base URL: https://api.windscribe.com/vpn (Base mainnet USDC) ## Buy and connect 1. GET /vpn returns prices, the payment network and token, and whether purchasing is `available`. 2. Create a WireGuard key pair, readable only by you: `umask 077; wg genkey | tee wg.key | wg pubkey`. The private key never leaves the machine. 3. POST /vpn/wireguard with JSON `{"wg_pubkey": "", "months": 1}`. Response: HTTP 402 with the base64 x402 PaymentRequired object in the PAYMENT-REQUIRED header (the same object is in the body). 4. Build an x402 v2 PaymentPayload for the single entry in `accepts`: scheme `exact`, an EIP-3009 transferWithAuthorization signed by the payer wallet. `accepts[0].extra` carries the USDC contract's EIP-712 domain name and version; use them as given. Retry the identical method, URL and body with the base64 payload in the PAYMENT-SIGNATURE header within `maxTimeoutSeconds` (60). The authorization's validBefore must be at most `maxTimeoutSeconds` (plus 30 seconds of clock skew) away, as x402 clients sign it; a longer one gets payment_payload_invalid. Any x402 v2 client works. Response: HTTP 201 with the entitlement and the [Interface] values in `wireguard`, plus its `psk` when the request is signed by the key (otherwise read `psk` from a signed GET /vpn/wireguard); the x402 SettlementResponse is in the PAYMENT-RESPONSE header. 5. Sign GET /vpn/wireguard/locations (see Request signing). Pick one `locations[].groups[]` entry and one of its `nodes[]`. 6. Write the WireGuard config below and bring the tunnel up: `wg-quick up ./windscribe.conf`. The purchase needs no request signature, so a plain x402 client can buy. The accepted requirement must equal the quote: the server never takes a client-supplied price, network, token, payee or expiry. Windscribe verifies the authorization with Coinbase's x402 facilitator, checks that the key can still be bought, then settles on Base; only a successful settlement creates, extends or reactivates access. Retrying the same PAYMENT-SIGNATURE is always safe: a settled payment returns the same entitlement without paying or extending again. Reusing a payment for a different key, method or term is rejected. A quote may be signed with the key being bought. A signed quote also checks that the key can buy that term now, so wireguard_key_exists or term_limit_exceeded arrive before the payer signs anything. An unsigned quote only returns the price. PUT quotes are always signed and checked. ## WireGuard config [Interface] PrivateKey = Address = , DNS = [Peer] PublicKey = PresharedKey = AllowedIPs = Endpoint = : PersistentKeepalive = 25 - `wireguard`, `psk` and `expires` come from a signed purchase or GET /vpn/wireguard response. An unsigned purchase response has no `psk`. - `Address` is derived from the public key; any other address is rejected by the node. - `wg_pubkey` belongs to the location group, and every node in the group shares it. Prefer nodes with lower `health` (load, 0 to 100). - Pass the last `hash` as `?hash=` to get `{"changed": 0}` when the inventory has not changed. - Static-IP nodes are never listed and refuse VPN API keys. ## Prices and terms - 1 to 7 months: USD 9 per month, paid in USDC. 12 months: USD 69. Terms of 8 to 11 months are not offered. - At most 12 months of access may be prepaid. `purchasable_terms` in every entitlement response lists the terms that fit now. - Buying time again with the same key extends it from max(now, expires). A revoked key is reactivated with a fresh PSK and a term starting now. - Months are calendar months: the same day and time N months later, and a day the month lacks rolls into the next month (31 January plus one month is 2 or 3 March). Access ends exactly at `expires` (Unix seconds); there is no grace period. - One-time settlement: no subscription, no automatic renewal, no automated refunds. - A key already used by a Windscribe account or a whitelabel partner cannot be bought. ## Manage (signed) - GET /vpn/wireguard: status, expiry, PSK and config values. - PUT /vpn/wireguard: buy time for the signing key, body `{"months": N}`. Quotes are signed and checked; sign the paid retry again over PAYMENT-SIGNATURE. - GET /vpn/wireguard/locations: the WireGuard exit inventory. Active entitlement only. - GET /vpn/wireguard/settings and PUT /vpn/wireguard/settings: ROBERT filters, body `{"robert": ["malware", "ads"]}`. Active entitlement only. - POST /vpn/wireguard/rotate: body `{"new_wg_pubkey": ""}`, signed by the current key. Moves the entitlement, or a paid credit, to the new key with a new PSK; the old key stops working and its tunnels end. The new key must be unused everywhere. - DELETE /vpn/wireguard: revoke immediately. Remaining time is forfeited unless the key buys time again. ## ROBERT DNS filtering Filter names: malware, ads, social, porn, gambling, fakenews, competitors, cryptominers. New entitlements start with ["malware"]. PUT /vpn/wireguard/settings replaces the list; an empty array disables filtering. Custom domain rules are not supported. `wireguard.dns` in entitlement responses is the resolver for the current filters, and filtering only applies to DNS sent to it. ## Request signing (wg-xeddsa-v1) Every /vpn/wireguard route except the purchase needs an RFC 9421 HTTP Message Signature made with the WireGuard private key: XEdDSA (Signal's specification) over the X25519 private scalar. The server converts the public key's Montgomery u to the Edwards y = (u - 1) / (u + 1) mod 2^255 - 19, with the sign bit 0, and verifies an ordinary Ed25519 signature. Headers: Content-Digest: sha-256=:BASE64_SHA256_OF_RAW_BODY: X-Windscribe-WireGuard-Key: BASE64_32_BYTE_WIREGUARD_PUBLIC_KEY Signature-Input: vpn=(...);created=...;expires=...;nonce="...";keyid="wg:...";alg="xeddsa-x25519-sha512";tag="windscribe-vpn-api-v1" Signature: vpn=:BASE64_64_BYTE_SIGNATURE: @method is the method as sent (GET, POST, PUT, DELETE), never lowercased. Covered components, in this order: "@method" "@scheme" "@authority" "@path" "@query" ["content-type"] "content-digest" "x-windscribe-wireguard-key" ["payment-signature"]. content-type is covered when present, payment-signature when present. - Content-Digest is required on every signed request; a GET or DELETE digests the empty body. The signature covers the exact raw body through it. - @scheme is always `https`, and @authority is always the API's canonical host (api.windscribe.com), whatever proxy the request travels through. - `created` and `expires` are Unix seconds. Lifetime at most 300 seconds; 60 seconds of clock skew allowed. - `nonce` is base64url without padding for exactly 32 random bytes, single use. - `keyid` is `wg:` followed by the canonical base64 public key. - Public keys are accepted as `wg pubkey` prints them; other encodings of the same key and small-order points get wireguard_key_invalid (400). - A request that sends any signature header must carry a complete, valid signature. Reference signer (Python 3.10 or newer, `pip install XEdDSA==1.2.0`): python3 sign_request.py --method GET --url https://api.windscribe.com/vpn/wireguard/locations --private-key-file wg.key Add `--body-file body.json --content-type application/json` for a body and `--payment-signature "$PAYMENT_SIGNATURE"` to sign a paid request. It prints the headers as JSON and never sends the private key anywhere. Send exactly the method, URL, body and headers it signed. It signs the URL's host without the default port (`--authority` names the canonical host when going through a proxy), and refuses URLs with credentials, another port or a fragment, header values with line breaks, key files readable by group or others, and reading a key it can't hide. The WireGuard private key is also the API credential: whoever holds it, or a config containing it, can change settings, revoke, or rotate the paid time to another key. Keep configs as private as the key; if one leaks, rotate at once. ## Responses and errors Every body is `{"data": {...}}`. Success carries `"success": 1`. Errors keep the HTTP status and carry `"success": 0`, a stable `error` code and a `message`, sometimes with extra fields. Branch on `error`, never on `message`; treat unknown codes by HTTP status. Every code, with what to do next: - vpn_api_disabled, vpn_api_not_configured (503): purchasing is off. Check GET /vpn later. - route_not_found (404): unknown path. Fix the request. - content_type_invalid (415): the body is not application/json. Send JSON. - field_missing (400): a required field is absent; `field` names it. Fix the request. - wireguard_key_invalid (400): not a WireGuard public key as `wg pubkey` prints it. Fix the key. - term_invalid, term_not_offered (400): unsupported `months`. Use a term from GET /vpn. - term_limit_exceeded (409): more than 12 months would be prepaid. Pick from `purchasable_terms`. - wireguard_key_exists (409): the key belongs to a Windscribe account or partner. Use a new key; no funds moved. - wireguard_key_mismatch (409): the signature is from another key than `wg_pubkey`. Sign with the key being bought. - wireguard_key_not_found (404): no entitlement for this key. Buy time first. - wireguard_entitlement_inactive (403): expired or revoked. Buy time to restore access. - wireguard_key_conflict (409): paid, but the key was registered elsewhere during settlement; `payment_id` names the payment. POST /vpn/wireguard/rotate signed by this key moves the paid term, starting now, to a new key. - wireguard_key_rotated (409): this payment's entitlement moved to another key. Use the new key. - wireguard_key_changed (409): the entitlement changed concurrently. Read the status, then retry. - rotate_same_key (400): `new_wg_pubkey` equals the current key. Use a new key. - payment_header_invalid, payment_payload_invalid (400): malformed PAYMENT-SIGNATURE. Rebuild the payment. - payment_requirement_mismatch, payment_resource_mismatch (409): the payment does not match the quote. Pay the current quote exactly. - payment_identifier_conflict (409): the payment was already used for another request. Sign a new payment. - payment_invalid, payment_settlement_failed (402): the facilitator rejected the payment; no funds moved. Fix the cause (balance, expiry) and sign a new payment. - payment_rejected (402): this payment was rejected before. Sign a new payment. - facilitator_unavailable (503): the facilitator was unreachable before settlement; no funds moved. Retry the same payment. - payment_in_progress, payment_state_changed (409): another payment for this key is running. Wait, then retry the same payment. - payment_reconciling (503): settlement's answer was lost, and the outcome is being confirmed on-chain. Retry the same request with the same PAYMENT-SIGNATURE in a minute: it answers 201 if the payment went through, or payment_rejected once it can't (then sign a new payment). It is never charged twice; don't sign a new payment while this is the answer. - signature_time_invalid (401): clock off by more than 60 seconds. Sync the clock and re-sign. - signature_missing, signature_invalid, signature_input_invalid, signature_profile_invalid, signature_components_invalid, signature_component_missing, signature_nonce_invalid, signature_lifetime_invalid, content_digest_invalid (401): the request signature is wrong. Re-sign (see Request signing). - signature_replayed (409): nonce already used. Re-sign with a fresh nonce. - settings_invalid, settings_filter_invalid (400): bad ROBERT list. Use the filter names above. - settings_sync_failed (503): saved, but the nodes are not updated yet. Retry the same settings. - replay_guard_unavailable, payment_lock_unavailable, facilitator_not_configured (503): a server dependency is down. Retry later. ## Privacy No account, email or Windscribe session is created or required. Windscribe stores the WireGuard public key, the generated PSK, the expiry, the ROBERT selection, and per payment its product fields, state and settlement transaction hash. It does not store the payer wallet address, payment signatures or request signatures. The settlement transaction is public on Base and names the paying wallet, so the stored hash links the key to that wallet: pay from a wallet you are comfortable linking to this purchase. ## Docs - [openapi.yaml](openapi.yaml): OpenAPI 3.1 contract for every endpoint - [sign_request.py](sign_request.py): reference wg-xeddsa-v1 request signer