# Stablegate Documentation — Full Bundle > Single-file export of every public Stablegate integration guide, verification rule, and API reference page. Intended for direct ingestion by LLM agents. For a curated index, see https://docs.stablegate.com/llms.txt. Base API URL: https://api.stablegate.com (version prefix /v1/). Authentication: every request sends the X-Api-Key HTTP header. Generated: 2026-11-11T04:05:04.121Z --- # Table of Contents - Introduction — https://docs.stablegate.com/docs/guide - Getting Started — https://docs.stablegate.com/docs/guide/getting-started - Coverage — https://docs.stablegate.com/docs/guide/getting-started/coverage - Authentication — https://docs.stablegate.com/docs/guide/getting-started/authentication - Idempotency — https://docs.stablegate.com/docs/guide/getting-started/idempotency - Postman Collection — https://docs.stablegate.com/docs/guide/getting-started/postman-collection - For LLM Agents — https://docs.stablegate.com/docs/guide/getting-started/llm-agents - Errors — https://docs.stablegate.com/docs/guide/getting-started/errors - Quickstart — https://docs.stablegate.com/docs/guide/getting-started/quickstart - Customers — https://docs.stablegate.com/docs/guide/customers - Exchange Routes — https://docs.stablegate.com/docs/guide/exchange-routes - Bank Providers — https://docs.stablegate.com/docs/guide/bank-providers - External Accounts — https://docs.stablegate.com/docs/guide/external-accounts - Fees & Limits — https://docs.stablegate.com/docs/guide/fees-and-limits - Fees — https://docs.stablegate.com/docs/guide/fees-and-limits/fees - Limits — https://docs.stablegate.com/docs/guide/fees-and-limits/limits - Orders — https://docs.stablegate.com/docs/guide/orders - Webhooks — https://docs.stablegate.com/docs/guide/webhooks - Integration Guides — https://docs.stablegate.com/docs/guide/integration-guides - Integration Checklist — https://docs.stablegate.com/docs/guide/integration-guides/checklist - Guide: On-Ramp (Fiat to Crypto) — https://docs.stablegate.com/docs/guide/integration-guides/onramp - Guide: Off-Ramp (Crypto to Fiat) — https://docs.stablegate.com/docs/guide/integration-guides/offramp - Common — https://docs.stablegate.com/docs/verification/common - Legal Notice — https://docs.stablegate.com/docs/verification/common/legal-notice - Terms of Business — https://docs.stablegate.com/docs/verification/common/terms - Privacy Policy — https://docs.stablegate.com/docs/verification/common/privacy - Reverse Solicitation Policy — https://docs.stablegate.com/docs/verification/common/reverse-solicitation-policy - Reverse Solicitation Declaration — https://docs.stablegate.com/docs/verification/common/reverse-solicitation-declaration - Cookie Notice — https://docs.stablegate.com/docs/verification/common/cookie-notice - Non-Serviced Countries — https://docs.stablegate.com/docs/verification/common/countries - Non-Serviced Business Sectors — https://docs.stablegate.com/docs/verification/common/business - Document Requirements — https://docs.stablegate.com/docs/verification/common/documents - Compliance — https://docs.stablegate.com/docs/verification/compliance - Individual Verification (KYC) — https://docs.stablegate.com/docs/verification/compliance/individual - Verification Process — https://docs.stablegate.com/docs/verification/compliance/individual/verification - Account Activation — https://docs.stablegate.com/docs/verification/compliance/individual/activation - Source of Funds — https://docs.stablegate.com/docs/verification/compliance/individual/sof - Business Verification (KYB) — https://docs.stablegate.com/docs/verification/compliance/business - Verification Process — https://docs.stablegate.com/docs/verification/compliance/business/verification - Account Activation — https://docs.stablegate.com/docs/verification/compliance/business/activation - Source of Funds — https://docs.stablegate.com/docs/verification/compliance/business/sof - Overview — https://docs.stablegate.com/docs/apiref - Authorization — https://docs.stablegate.com/docs/apiref/auth - Changelog — https://docs.stablegate.com/docs/changelog - OpenAPI Endpoint Summary — https://docs.stablegate.com/docs/apiref --- --- # Introduction Source: https://docs.stablegate.com/docs/guide Stablegate is a processing infrastructure that enables businesses to offer seamless exchanges between fiat currencies and digital assets. Through a single API, your platform can provide on-ramp (fiat-to-crypto) and off-ramp (crypto-to-fiat) services to your customers. ## How It Works ```mermaid flowchart LR P[Your platform] --> A[Stablegate API] --> X[Exchange processing] --> S[Settlement] S --> F["Fiat networks
(SEPA)"] S --> B["Blockchain networks
(Ethereum, Polygon, ...)"] ``` 1. You register your end-user as a customer through the Stablegate API. 2. Your customer initiates an exchange through your platform. 3. Your backend calls the Stablegate API to create an order. 4. Stablegate processes the exchange, handling all settlement and compliance. 5. Funds are delivered to the destination — a bank account or crypto wallet. ## Key Concepts ### Rails A **rail** is a payment network used to move funds. Rails fall into two categories: | Type | Rails | Description | |------|-------|-------------| | **Bank** | `sepa` | Single Euro Payments Area — EUR bank transfers across Europe | | **Crypto** | `ethereum`, `tron`, `stellar` | Blockchain networks; which stablecoin moves on which network is listed under [Supported Exchange Routes](#supported-exchange-routes) | ### Assets An **asset** is a currency that moves through a rail: | Asset Code | Type | Description | |------------|------|-------------| | `EUR` | Fiat | Euro | | `USDT` | Stablecoin | Tether USD on Ethereum (ERC-20) or Tron (TRC-20) | | `USDC` | Stablecoin | USD Coin on Ethereum (ERC-20) or Stellar | | `EURC` | Stablecoin | Euro Coin on Ethereum (ERC-20) | ### Exchange Routes An **exchange route** is a specific combination of source and destination, each defined by a rail and an asset. A route represents a conversion path that Stablegate supports. ### On-Ramp and Off-Ramp - **On-ramp**: Fiat to crypto. The customer sends fiat currency (e.g., EUR via SEPA) and receives digital assets (e.g., USDC on Ethereum). - **Off-ramp**: Crypto to fiat. The customer sends digital assets (e.g., USDT on Ethereum) and receives fiat currency (e.g., EUR to their bank account). ### Customers A **customer** is your end-user, registered via the API and identified by a UUID. All operations — orders, accounts, limits — are scoped to a customer. ### Bank Providers A **bank provider** is a financial institution that processes fiat transactions on a specific rail (e.g., SEPA, SWIFT). Use the [Bank Providers API](/docs/guide/bank-providers) to discover which banks are available, what rails they support, and which exchange routes are accessible through each bank. ### External Accounts Before creating orders, you must register your customer's **external accounts** — the bank accounts (IBANs) or crypto wallets where funds will be sent or received. ### Orders An **order** represents a single exchange operation. Each order has a lifecycle with defined statuses, from creation through processing to completion. ## Supported Exchange Routes | Direction | Source | Destination | |-----------|--------|-------------| | On-ramp | EUR (SEPA) | USDT (Ethereum or Tron) | | On-ramp | EUR (SEPA) | USDC (Ethereum or Stellar) | | On-ramp | EUR (SEPA) | EURC (Ethereum) | | Off-ramp | USDT (Ethereum or Tron) | EUR (SEPA) | | Off-ramp | USDC (Ethereum or Stellar) | EUR (SEPA) | | Off-ramp | EURC (Ethereum) | EUR (SEPA) | > **Note:** The list of supported routes is dynamic. Always query the API to get the current list of available routes. See [Coverage](/docs/guide/getting-started/coverage) for the customer types, verification paths and accounts the API supports, and the [Integration Checklist](/docs/guide/integration-guides/checklist) for the path from first request to go-live. ## Base URL All API requests are made to: ```text https://api.stablegate.com ``` ## API Version The current API version is **v1**. All endpoints are prefixed with `/v1/`. --- # Getting Started Source: https://docs.stablegate.com/docs/guide/getting-started This guide walks you through setting up your Stablegate API access and making your first API call. In a hurry? [Quickstart](/docs/guide/getting-started/quickstart) is the whole path on one page: key, customer, verification, order, webhook. ## Prerequisites To get started, you need a Stablegate account with Partner API access enabled. Sign in with your usual Stablegate credentials and open Developers (https://app.stablegate.com/developers). The menu entry appears only once access is granted, so use the link until then. Without access the page shows a **Developer API** card with a **Request API access** button; submit the request from there. If you do not see the button, contact your account manager. Once access is granted, the page opens on three tabs: **API keys**, **Webhooks** and **Your clients**. ## Step 1: Open the Developers Section Open Developers (https://app.stablegate.com/developers). The section opens on the **API keys** tab. ## Step 2: Create an API Key 1. Once signed in, open the **API keys** tab. 2. Click **Create key**. 3. Give your key a descriptive name (e.g., "Production Backend" or "Staging Environment"). 4. Copy and store both the key and the secret. The secret is shown once, at creation. The key is what you send in `X-Api-Key`. The secret is only needed if you sign your requests, which the Partner API does not currently require. > **Warning:** Treat your API key as a secret. Do not expose it in client-side code, public repositories, or logs. ## Step 3: Make Your First API Call Let's verify your setup by fetching the list of supported exchange routes. **Request:** ```bash curl https://api.stablegate.com/v1/exchange-routes/ \ --header 'X-Api-Key: YOUR_API_KEY' ``` **Response:** ```json [ { "source": { "rail": "sepa", "asset": { "code": "EUR", "fraction_digits": 2 } }, "destination": { "rail": "ethereum", "asset": { "code": "USDC", "fraction_digits": 6 } } }, { "source": { "rail": "ethereum", "asset": { "code": "USDT", "fraction_digits": 6 } }, "destination": { "rail": "sepa", "asset": { "code": "EUR", "fraction_digits": 2 } } } ] ``` If you receive a `200` response with exchange route data, your API key is working correctly. ## Request Format The API ignores unknown fields in a JSON request body instead of rejecting them. A misspelled or misplaced optional field is silently skipped, and the API uses its default value instead. To catch such mistakes before you send a request, generate your client from the [OpenAPI specification](/openapi.json), which defines every field of every endpoint. ## Integration Flow Overview A typical integration follows this sequence. See the [Customers](/docs/guide/customers), [External Accounts](/docs/guide/external-accounts), [Fees & Limits](/docs/guide/fees-and-limits), and [Orders](/docs/guide/orders) sections for details on each step. 1. **Create a customer** — register your end-user in the Stablegate system. 2. **Register external accounts** — bank accounts and/or crypto wallets. 3. **Check exchange route** — verify the conversion path is available. 4. **Browse bank providers** — discover banks, rails and exchange routes. 5. **Check fees** — calculate the fees and exchange rate. 6. **Check limits** — verify the customer's transaction limits. 7. **Create order** — submit the exchange order. 8. **Set up webhooks (optional)** — receive real-time notifications on status changes. 9. **Monitor order status** — via webhooks or by polling the order endpoint. --- # Coverage Source: https://docs.stablegate.com/docs/guide/getting-started/coverage What the Partner API supports today, and how the work is split between your integration and Stablegate. Routes, assets and rules change over time: the API is the source of truth at runtime, and the [Changelog](/docs/changelog) records every change. ## Exchange Routes The fiat side is `EUR` over `sepa`. The crypto side is a stablecoin on one of its supported networks: | Asset | Networks | |-------|----------| | `USDT` | `ethereum`, `tron` | | `USDC` | `ethereum`, `stellar` | | `EURC` | `ethereum` | Every pair works in both directions: on-ramp (`EUR` to the stablecoin) and off-ramp (the stablecoin to `EUR`). Each order is one exchange between one source and one destination. Query [`GET /v1/exchange-routes/`](/docs/guide/exchange-routes) for the current list and `fraction_digits` of each asset, and [`POST /v1/exchange-routes/check/`](/docs/guide/exchange-routes#check-route-availability) before creating an order: a route can exist and still be temporarily unavailable. ## Customers | | Individual | Corporate | |---|---|---| | Create via API | Yes | Yes (`company_name` required) | | [Instant Verification](/docs/guide/customers#start-instant-verification) | Yes, when enabled for your account | No | | [Standard verification](/docs/guide/customers#create-a-verification-link) via hosted link | Yes | Yes | | Trading after approval | Instant: at once. Standard: after activation | After activation | Instant Verification runs sanctions and PEP screening and approves automatically when the screening is clear, usually within minutes; a match goes to manual review. Standard verification adds document checks and account activation (a test deposit or a video interview). Higher levels and personal limits are assigned by Stablegate compliance after a Source-of-Funds review. A customer record is created once and is not edited afterwards; a customer that has not started verification can be closed (see [Close a Customer](/docs/guide/customers#close-a-customer)). Customer states (`active`, `limited`, `blocked`, `closed`) are managed by Stablegate and read from [`GET /v1/customers/{customer_uuid}/`](/docs/guide/customers#get-customer-details). Customers from the [non-serviced countries](/docs/verification/common/countries) and US persons cannot be onboarded. ## Accounts | Account | Networks and rails | Ownership proof | |---------|--------------------|-----------------| | Bank account | IBAN, SEPA payouts | Holder name on the account | | Wallet | `ethereum`, `tron`, `stellar` for the routes above; see [External Accounts](/docs/guide/external-accounts#create-a-wallet) for every accepted rail | Signed message for Ethereum, or a [Satoshi Test](/docs/guide/external-accounts#satoshi-test) | Every order pays out to an account registered for the customer. The number of accounts per customer is set by your account configuration. See [External Accounts](/docs/guide/external-accounts). ## Limits and Fees Limits are per customer, counted in EUR, and depend on the verification level and risk category. The fee is calculated in the deposit currency, deducted from the deposit, and has a minimum per transaction. Defaults are on [Limits](/docs/guide/fees-and-limits/limits) and [Fees](/docs/guide/fees-and-limits/fees); the values that apply to a given customer come from [`GET /v1/customers/{customer_uuid}/limits/`](/docs/guide/fees-and-limits/limits#get-customer-limits) and [`POST /v1/customers/{customer_uuid}/check-fees/`](/docs/guide/fees-and-limits/fees#check-fees-api). ## Events [Webhooks](/docs/guide/webhooks) deliver `order.status_changed` for orders created through the API, signed with HMAC-SHA512. Verification outcome, customer state and deposit progress are read by polling the customer and order detail endpoints. ## Who Does What | Through the API | By Stablegate, on request | |-----------------|---------------------------| | Create customers, start verification, close unused customers | Enable Partner API access and review clients' verification | | Register and remove bank accounts and wallets | Block, limit, reactivate or close a verified customer | | Check routes, fees and limits; create and cancel orders while `new` | Set verification levels, custom limits, minimum amounts and fee schedules | | Read order and deposit status; receive webhooks | Retry, hold, stop or roll back an order; refund a rejected deposit; recover a transfer sent on the wrong network | | Manage API keys and webhooks in the Developers section | Set the deposit deadline (`valid_until`) and the number of accounts per customer | To request anything from the right column, contact your account manager or support with the customer or order UUID. --- # Authentication Source: https://docs.stablegate.com/docs/guide/getting-started/authentication Every request to the Stablegate API must be authenticated using an API key. ## API Key Authentication Include your API key in the `X-Api-Key` header with every request: | Header | Description | |--------|-------------| | `X-Api-Key` | Your API key, generated in the Developers (https://app.stablegate.com/developers) section | **Example request:** ```bash curl https://api.stablegate.com/v1/exchange-routes/ \ --header 'X-Api-Key: YOUR_API_KEY' ``` ## Request Paths Every endpoint path ends with a slash, for example `/v1/customers/`, not `/v1/customers`. Call the exact path from the API reference: a path without the trailing slash is answered with a `307` redirect to the same path with the slash instead of the response, which costs an extra round trip, and a client that does not follow redirects gets the `307` itself. ## Creating an API Key 1. Sign in to the Stablegate app and open Developers (https://app.stablegate.com/developers). 2. Open the **API keys** tab. 3. Click **Create key** and provide a descriptive name. 4. Copy and store the key and the secret; the secret is shown once, at creation. Only the key goes into `X-Api-Key`; the secret is for signing requests, which is optional today. You can create multiple API keys to separate environments or services (e.g., one for staging, one for production). ## Security Best Practices - **Keep your API key secret.** Never expose it in client-side code, browser requests, or version control. - **Use environment variables** to store API keys in your application. - **Rotate keys periodically.** Create a new key, switch your integration over to it, then delete the old one. Deletion takes effect immediately, so delete only after the new key is in use. - **Use separate keys** for different environments (development, staging, production). ## Code Examples ### JavaScript (Node.js) ```javascript const API_KEY = process.env.STABLEGATE_API_KEY; const BASE_URL = "https://api.stablegate.com"; async function request(method, path, body = null) { const options = { method, headers: { "X-Api-Key": API_KEY, "Content-Type": "application/json", }, }; if (body) { options.body = JSON.stringify(body); } const response = await fetch(`${BASE_URL}${path}`, options); if (!response.ok) { throw new Error(`API error: ${response.status} ${response.statusText}`); } if (response.status === 204) return null; return response.json(); } // Usage const routes = await request("GET", "/v1/exchange-routes/"); ``` ### Python ```python import os import requests API_KEY = os.environ["STABLEGATE_API_KEY"] BASE_URL = "https://api.stablegate.com" def request(method, path, json=None): response = requests.request( method, f"{BASE_URL}{path}", headers={ "X-Api-Key": API_KEY, "Content-Type": "application/json", }, json=json, ) response.raise_for_status() if response.status_code == 204: return None return response.json() # Usage routes = request("GET", "/v1/exchange-routes/") ``` ### cURL ```bash # GET request curl -X GET https://api.stablegate.com/v1/exchange-routes/ \ -H "X-Api-Key: YOUR_API_KEY" # POST request with JSON body curl -X POST https://api.stablegate.com/v1/exchange-routes/check/ \ -H "X-Api-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "source": { "rail": "sepa", "asset_code": "EUR" }, "destination": { "rail": "ethereum", "asset_code": "USDC" } }' ``` ## Error Responses If authentication fails, the API returns a `401 Unauthorized` or `403 Forbidden` response: ```json { "trace_id": "3f2504e0-4f89-11d3-9a0c-0305e82c3301", "error_code": "devapi_unauthorized_error", "message": "Unauthorized" } ``` The body of every error, the codes and what to do about them are on [Errors](/docs/guide/getting-started/errors). **Common causes:** - Missing `X-Api-Key` header - Invalid or revoked API key - API key does not have access to the requested resource - The Partner API is not enabled for your account — the API answers `403` A `503` on an authenticated request means Stablegate could not verify the key because an internal service was unavailable. The key itself is fine: retry the request. --- # Idempotency Source: https://docs.stablegate.com/docs/guide/getting-started/idempotency The Stablegate API supports idempotent requests, allowing you to safely retry operations without risk of duplicating side effects. This is essential for payment processing where network failures, timeouts, or client crashes can leave you uncertain whether a request succeeded. ## How It Works Include an `Idempotency-Key` header with any `POST`, `PUT`, or `PATCH` request. The API uses this key to recognize retries of the same request: | Header | Description | |--------|-------------| | `Idempotency-Key` | A unique string identifying the request (max 255 printable ASCII characters) | ```bash curl https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/orders/ \ --request POST \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --header 'Idempotency-Key: ord_2025-01-15_abc123' \ --data '{ "source": { "rail": "sepa", "asset": "EUR", "amount": 1000 }, "destination": { "rail": "ethereum", "asset": "USDC", "client_account_uuid": "wa1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d" } }' ``` ### Request Lifecycle ```mermaid flowchart TD A[Client sends POST with Idempotency-Key] --> B{Key seen before?} B -->|No| C[Process request] --> D[Cache result] --> E[Return response] B -->|Yes| F{Same request body?} F -->|Yes| G[Replay cached response] F -->|No| H["422 Error (key misuse)"] ``` 1. **First request** — The API processes the request normally, caches the response, and returns the result. 2. **Retry with same key and same body** — The API returns the cached response with an `Idempotency-Replay: true` header. No side effects are repeated. 3. **Same key, different body** — The API returns `422` to prevent accidental misuse of keys. 4. **No key provided** — The request is processed normally without idempotency guarantees. ## Generating Keys Use any unique string up to 255 printable ASCII characters. We recommend **UUID v4** or a compound key derived from your internal identifiers: ```javascript // Option 1: UUID v4 const idempotencyKey = crypto.randomUUID(); // Option 2: Compound key (recommended for order creation) const idempotencyKey = `order_${customerId}_${internalOrderId}`; ``` ```python import uuid # Option 1: UUID v4 idempotency_key = str(uuid.uuid4()) # Option 2: Compound key idempotency_key = f"order_{customer_id}_{internal_order_id}" ``` > **Tip:** Using compound keys tied to your business logic (e.g., customer ID + internal order ID) naturally prevents duplicate orders even across application restarts. ## Replayed Responses When a response is served from cache, the API adds the `Idempotency-Replay` header: ```text Idempotency-Replay: true ``` The status code and response body are identical to the original response. Your client can inspect this header for logging or debugging, but should otherwise treat the response as normal. ## Key Retention Idempotency keys and their cached results are retained for **24 hours**. After this period, the key is released and can be reused. ## Error Handling ### `422` — Invalid key format ```json { "trace_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "error_code": "devapi_idempotency_invalid_key", "message": "Idempotency-Key contains invalid characters or exceeds 255 characters." } ``` The key must consist of printable ASCII characters (codes 33–126) and not exceed 255 characters. ### `422` — Key reused with different request ```json { "trace_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "error_code": "devapi_idempotency_key_misuse", "message": "The Idempotency-Key has already been used with a different request payload. Use a new key for a different request." } ``` Each idempotency key is bound to a specific combination of HTTP method, URL path, and request body. Reusing a key with different parameters is rejected to prevent unintended behavior. ### `409` — Concurrent request in progress ```json { "trace_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "error_code": "devapi_idempotency_request_in_progress", "message": "A request with this Idempotency-Key is currently being processed. Retry after it completes." } ``` Another request with the same key is still being processed. Wait briefly and retry. ## What Gets Cached - **Successful responses** (2xx) and **client error responses** (4xx) are cached and replayed on retry. - **Server errors** (5xx) are **not cached**. The key is released so you can retry the same request. This means you can safely retry after a timeout or network error: if the server processed your request successfully, you'll get the cached result; if it failed with a 5xx, the retry will be processed as a new attempt. ## Which Requests Need Idempotency Keys Idempotency keys are supported on all `POST`, `PUT`, and `PATCH` requests. They are most important for operations with financial side effects: | Operation | Recommended | |-----------|-------------| | Create Customer | **Yes** — prevents duplicate customer registration | | Create Order | **Yes** — prevents duplicate orders | | Create Bank Account | Yes — prevents duplicate account registration | | Create Wallet | Yes — prevents duplicate wallet registration | | Check Fees | Optional — read-only, safe to repeat | | Check Limits | Optional — read-only, safe to repeat | | Check Exchange Route | Optional — read-only, safe to repeat | `GET` and `DELETE` requests are inherently idempotent and do not require keys. ## Retry Strategy When implementing retries, we recommend: 1. **Always include an `Idempotency-Key`** for order creation and account registration requests. 2. **Generate the key before the first attempt** and reuse the same key for all retries of that operation. 3. **Retry on network errors and 5xx responses** — the API guarantees safe retries with the same key. 4. **Do not retry 4xx responses** (except `409`) — these indicate a client error that won't resolve on retry. 5. **Use exponential backoff** — wait 1s, 2s, 4s between retries to avoid overwhelming the API. ```javascript async function requestWithRetry(method, path, body, idempotencyKey, maxRetries = 3) { for (let attempt = 0; attempt <= maxRetries; attempt++) { try { const response = await fetch(`${BASE_URL}${path}`, { method, headers: { "X-Api-Key": API_KEY, "Content-Type": "application/json", "Idempotency-Key": idempotencyKey, }, body: JSON.stringify(body), }); if (response.status === 409) { // Concurrent request — wait and retry await new Promise(r => setTimeout(r, 1000 * Math.pow(2, attempt))); continue; } if (response.status >= 500) { // Server error — safe to retry await new Promise(r => setTimeout(r, 1000 * Math.pow(2, attempt))); continue; } return response; } catch (error) { // Network error — safe to retry if (attempt === maxRetries) throw error; await new Promise(r => setTimeout(r, 1000 * Math.pow(2, attempt))); } } } ``` --- # Postman Collection Source: https://docs.stablegate.com/docs/guide/getting-started/postman-collection We provide a ready-to-use Postman collection with every Stablegate API endpoint and example request bodies. It is generated from the same [OpenAPI specification](/openapi.json) that powers the [API Reference](/docs/apiref), so it always matches the current API. Import it into Postman to start testing immediately. ## Download :u-button{label="Download Stablegate API Collection (JSON)" icon="i-lucide-download" to="/downloads/stablegate-api.postman_collection.json" download external} ## Import into Postman 1. Open Postman. 2. Click **Import** in the top-left corner. 3. Drag and drop the downloaded JSON file, or click **Upload Files** and select it. 4. The "Stablegate API" collection will appear in your sidebar. ## Configure Environment Variables After importing, set up your environment variables before making requests. ### Create an Environment 1. Click the **Environments** tab in the left sidebar. 2. Click **Create Environment** (or the `+` button). 3. Name it "Stablegate" (or "Stablegate Production", etc.). 4. Add the following variables: | Variable | Type | Initial Value | Description | |----------|------|---------------|-------------| | `base_url` | default | `https://api.stablegate.com` | API base URL | | `api_key` | secret | `YOUR_API_KEY` | Your API key from the **Developers** section | | `customer_uuid` | default | *(your customer UUID)* | A customer UUID for testing | | `account_uuid`, `wallet_uuid`, `order_uuid` | default | *(optional)* | Path parameters for single-resource requests; fill in as you create resources | Click **Save**, then select the "Stablegate" environment from the environment dropdown (top-right corner). ## Collection Structure One folder per API tag, one request per endpoint: ### Customers | Request | Method | Path | |---------|--------|------| | Create Customer | `POST` | `/v1/customers/` | | Get Customers List | `GET` | `/v1/customers/` | | Get Customer | `GET` | `/v1/customers/{customer_uuid}/` | | Close Customer | `DELETE` | `/v1/customers/{customer_uuid}/` | | Download Account Opening Letter | `GET` | `/v1/customers/{customer_uuid}/account-opening-letter/` | ### Verifications | Request | Method | Path | |---------|--------|------| | Get Customer Verification | `GET` | `/v1/customers/{customer_uuid}/verification/` | | Create Verification Link | `POST` | `/v1/customers/{customer_uuid}/verification/link/` | | Import Verification Data | `POST` | `/v1/customers/{customer_uuid}/verification/import/` | | Start Instant Verification | `POST` | `/v1/customers/{customer_uuid}/verification/instant/` | ### Orders | Request | Method | Path | |---------|--------|------| | Create Order | `POST` | `/v1/customers/{customer_uuid}/orders/` | | Get Orders | `GET` | `/v1/customers/{customer_uuid}/orders/` | | Create Verification Order | `POST` | `/v1/customers/{customer_uuid}/orders/verification/` | | Get Order | `GET` | `/v1/customers/{customer_uuid}/orders/{order_uuid}/` | | Cancel Order | `POST` | `/v1/customers/{customer_uuid}/orders/{order_uuid}/cancel/` | | Attach Invoice | `POST` | `/v1/customers/{customer_uuid}/orders/{order_uuid}/third-party-document/` | ### Fees | Request | Method | Path | |---------|--------|------| | Check Customer Fees | `POST` | `/v1/customers/{customer_uuid}/check-fees/` | ### Limits | Request | Method | Path | |---------|--------|------| | Customer Limits | `GET` | `/v1/customers/{customer_uuid}/limits/` | | Check Customer Limits | `POST` | `/v1/customers/{customer_uuid}/check-limits/` | ### Bank accounts | Request | Method | Path | |---------|--------|------| | Get Bank Accounts | `GET` | `/v1/customers/{customer_uuid}/bank-accounts/` | | Create Bank Account | `POST` | `/v1/customers/{customer_uuid}/bank-accounts/` | | Get Bank Account | `GET` | `/v1/customers/{customer_uuid}/bank-accounts/{account_uuid}/` | | Update Bank Account | `PUT` | `/v1/customers/{customer_uuid}/bank-accounts/{account_uuid}/` | | Delete Bank Account | `DELETE` | `/v1/customers/{customer_uuid}/bank-accounts/{account_uuid}/` | | Get Bank Info By Iban | `GET` | `/v1/bank-accounts/info-by-iban/` | ### Customer Wallets | Request | Method | Path | |---------|--------|------| | Get Account For Satoshitest | `GET` | `/v1/customers/{customer_uuid}/wallets/satoshitest/` | | Get Customer Wallets List | `GET` | `/v1/customers/{customer_uuid}/wallets/` | | Create Customer Wallet | `POST` | `/v1/customers/{customer_uuid}/wallets/` | | Get Customer Wallet | `GET` | `/v1/customers/{customer_uuid}/wallets/{wallet_uuid}/` | | Update Customer Wallet | `PUT` | `/v1/customers/{customer_uuid}/wallets/{wallet_uuid}/` | | Delete Customer Wallet | `DELETE` | `/v1/customers/{customer_uuid}/wallets/{wallet_uuid}/` | ### Exchange Routes | Request | Method | Path | |---------|--------|------| | Get All Supported Exchange Routes | `GET` | `/v1/exchange-routes/` | | Check Exchange Route | `POST` | `/v1/exchange-routes/check/` | ### Bank Providers | Request | Method | Path | |---------|--------|------| | Get Bank Providers | `GET` | `/v1/bank-providers/` | ## Testing Workflow Use the collection in the following order for a complete integration test: **On-ramp (EUR → USDC):** 1. **Customers** → Create Customer *(register your end-user)* 2. **Exchange Routes** → List Exchange Routes 3. **Exchange Routes** → Check Exchange Route *(set source to SEPA/EUR, destination to Ethereum/USDC)* 4. **Crypto Wallets** → Create Wallet *(register the destination wallet)* 5. **Fees** → Check Fees *(calculate costs)* 6. **Limits** → Check Limits *(verify customer capacity)* 7. **Orders** → Create Order *(submit the on-ramp order)* 8. **Orders** → Get Order *(poll for status updates)* **Off-ramp (USDT → EUR):** 1. **Customers** → Create Customer *(if not already created)* 2. **Exchange Routes** → Check Exchange Route *(set source to Ethereum/USDT, destination to SEPA/EUR)* 3. **Bank Accounts** → Create Bank Account *(register the destination IBAN)* 4. **Fees** → Check Fees 5. **Limits** → Check Limits 6. **Orders** → Create Order *(submit the off-ramp order)* 7. **Orders** → Get Order *(poll for status updates)* 8. **Orders** → Cancel Order *(optional — cancel before deposit)* ## Tips - **Use Collection Variables:** The collection includes placeholder variables like `{{base_url}}`, `{{api_key}}`, and `{{customer_uuid}}`. Set these in your environment to avoid editing each request manually. - **Save UUIDs from responses:** After creating a customer, bank account, or wallet, copy the returned `uuid` and paste it into subsequent requests. The `customer_uuid` from the Create Customer response is used in all customer-scoped endpoints. - **Check the Console:** Open Postman's Console (`View → Show Postman Console`) to inspect full request/response details for debugging. --- # For LLM Agents Source: https://docs.stablegate.com/docs/guide/getting-started/llm-agents Stablegate publishes a set of machine-readable resources for teams that use LLM coding agents (Claude, Cursor, Copilot, Windsurf, and similar tools) to build integrations against our API. Point your agent at these URLs instead of scraping the rendered documentation. ## Resources | Resource | URL | Use it for | |----------|-----|------------| | `llms.txt` | [docs.stablegate.com/llms.txt](https://docs.stablegate.com/llms.txt) | Curated index of integration-relevant pages. Small, fits in a single prompt. Follows the [llmstxt.org](https://llmstxt.org/) specification. | | `llms-full.txt` | [docs.stablegate.com/llms-full.txt](https://docs.stablegate.com/llms-full.txt) | Full documentation bundle in one file: every guide, verification rule, and a compact OpenAPI endpoint summary. Intended for RAG ingestion or single-shot context loading. | | `openapi.json` | [docs.stablegate.com/openapi.json](https://docs.stablegate.com/openapi.json) | Raw OpenAPI 3.1 specification. Use this for code generation or schema validation. | ## Which resource to use - **Interactive assistants with limited context** (IDE plugins, chat assistants) — start with `llms.txt`. It fits in a single prompt and tells the agent where to look next. - **Retrieval-augmented generation and fine-tuning pipelines** — ingest `llms-full.txt`. One file, one request, all integration knowledge included. - **Code generators and SDK scaffolding** — consume `openapi.json` directly. The same spec is rendered as the [API Reference](/docs/apiref) on this site. ## Example Fetch the curated index from the command line: ```bash curl -s https://docs.stablegate.com/llms.txt ``` Or supply it to your agent as part of the system prompt: ```bash curl -s https://docs.stablegate.com/llms-full.txt > stablegate-docs.txt # Attach stablegate-docs.txt to your agent's context ``` ## Update cadence All three files are regenerated on every documentation release. The content is always in sync with this site and with the API contract published on `api.stablegate.com`. --- # Errors Source: https://docs.stablegate.com/docs/guide/getting-started/errors Every failed request returns the same JSON body. ```json { "trace_id": "3f2504e0-4f89-11d3-9a0c-0305e82c3301", "error_code": "devapi_not_found_error", "message": "Not Found", "details": "The customer does not belong to this partner" } ``` | Field | What it is | |---|---| | `trace_id` | Identifier of this request. Keep it: support asks for it first | | `error_code` | Machine-readable code, stable across releases. Branch on it | | `message` | Short explanation for a human. The wording changes without notice, do not match on it | | `details` | Extra context: a string, a list of field errors, or no key at all when there is nothing to add | ## Status codes | Status | Meaning | What to do | |---|---|---| | `400` | The request is well-formed, but these values cannot be used | Read `message` and fix the request | | `401` | The API key is missing or unknown | Send a valid `X-Api-Key` | | `403` | The key is valid, the action is not allowed for it | Do not retry; the action needs a different account | | `404` | The object does not exist or belongs to another partner | Check the UUID against your own customers | | `405` | The method is not allowed on this path | Fix the request | | `409` | The object is in a state that does not allow the action | Read the object, act on its current state | | `422` | The body or a header failed validation | Read `details`; the list names every invalid field | | `500` | A failure on our side | Retry with backoff; if it repeats, send support the `trace_id` | | `503` | The service is temporarily unavailable | Retry with backoff | ## Codes | `error_code` | HTTP | When | |---|---|---| | `devapi_internal_error` | `500` | Unexpected failure inside Stablegate | | `devapi_request_validation_error` | `422` | The body or a header failed validation | | `devapi_iban_parse_error` | `400` | The IBAN cannot be parsed | | `devapi_bank_code_required_error` | `400` | The IBAN does not carry a BIC; pass `bank_code` explicitly | | `devapi_bad_request_error` | `400` | The values contradict each other, for example both `client_account` and `client_account_uuid` | | `devapi_email_occupied_error` | `400` | The email or username belongs to an existing customer | | `devapi_not_found_error` | `404` | The object does not exist or is not yours | | `devapi_unauthorized_error` | `401` | The API key is missing or unknown | | `devapi_forbidden_error` | `403` | The action is not allowed for this key | | `devapi_method_not_allowed_error` | `405` | The method is not allowed on this path | | `devapi_customer_close_error` | `409` | A customer can be closed only in the `new` state with no verification submitted | | `devapi_download_aol_error` | `409` | The account opening letter is not available in the current state | | `devapi_kyc_application_link_error` | `400`, `409` | Verification by link is not configured, or the application is in a state that does not allow the action | | `devapi_instant_verification_error` | `400`, `409` | Instant verification is not configured, not available for this customer type, or the application is in a state that does not allow the action | | `devapi_idempotency_invalid_key` | `422` | `Idempotency-Key` has non-printable characters or is longer than 255 | | `devapi_idempotency_key_misuse` | `422` | The key was already used with a different body or path | | `devapi_idempotency_request_in_progress` | `409` | A request with this key is still running; retry when it finishes | | `devapi_service_unavailable_error` | `503` | A service Stablegate depends on is down | ## Validation errors A `422` lists every invalid field in `details`: ```json { "trace_id": "3f2504e0-4f89-11d3-9a0c-0305e82c3301", "error_code": "devapi_request_validation_error", "message": "Unprocessable Entity", "details": [ { "loc": ["body", "source", "amount"], "msg": "Input should be a valid decimal", "type": "decimal_parsing" }, { "loc": ["body", "destination", "client_account_uuid"], "msg": "Field required", "type": "missing" } ] } ``` `loc` is the path to the field: the first item is where the value came from (`body`, `query`, `header`), the rest is the path inside it. ## Codes raised by other services Requests that touch orders, limits, accounts, customers or verification go through internal services, and their codes reach you through the Partner API: `oa_` for operations, `ca_` for customers. An example is `oa_limit_exceeded_error`, which `check-limits` and order creation return when a rule is broken, with the reason in `details`. Treat these codes as opaque. Log the code and the `trace_id`, show the customer `message`, and send both to support when the problem needs us. ## Retries - Retry `500`, `503` and timeouts with exponential backoff. Three attempts is enough for an interactive flow; a background job can keep going longer. - Send the same `Idempotency-Key` on every retry of the same request: a repeated create returns the first result instead of a second order. A retry with a different body answers `422` with `devapi_idempotency_key_misuse`. - Do not retry `400`, `401`, `403`, `404` and `422` unchanged: the same request fails the same way. - `409` may go away when the object changes state, so re-read the object before you decide. - There is no rate limit today. If one appears, it will answer `429` with `Retry-After`. ## The request id header Every response carries `X-Request-Id`. Send your own value in the request header and Stablegate returns it unchanged; without it the platform generates one. The `trace_id` of an error body is the same value, so the header alone identifies the call even when the body never reaches you — for example a gateway timeout. ```bash curl https://api.stablegate.com/v1/customers/ \ -H "X-Api-Key: YOUR_API_KEY" \ -H "X-Request-Id: order-c1d2e3f4-42" ``` ## Before you contact support Send the `trace_id`, the method and path of the request, and the time with the time zone. The `trace_id` finds the request in our logs, everything else narrows the search. --- # Quickstart Source: https://docs.stablegate.com/docs/guide/getting-started/quickstart Seven requests take a new customer from nothing to a paid-out on-ramp order. Each step links to the page that explains the details; the bodies here are the shortest working form. Send `X-Api-Key` with every request — see [Authentication](/docs/guide/getting-started/authentication) for where the key comes from and for the client samples in other languages. ## 1. Create the customer ```bash curl https://api.stablegate.com/v1/customers/ \ --request POST \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "type": "individual", "email": "max.mustermann@example.com", "first_name": "Max", "last_name": "Mustermann", "country_code": "DE" }' ``` The response carries the customer `uuid`. Keep it: every later call needs it. Details — [Customers](/docs/guide/customers#create-a-customer). ## 2. Verify the customer Instant verification needs nothing from the customer beyond their identity details: ```bash curl https://api.stablegate.com/v1/customers/CUSTOMER_UUID/verification/instant/ \ --request POST \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "first_name": "Max", "last_name": "Mustermann", "date_of_birth": "1990-05-17" }' ``` Stablegate screens the name against sanctions and politically exposed persons and approves the application when the screening is clear — usually within minutes. Poll [Check the Verification State](/docs/guide/customers#check-the-verification-state) until `application.status` is `approved`; `kyc_level_key` then holds `light`. ```bash curl https://api.stablegate.com/v1/customers/CUSTOMER_UUID/verification/ \ --header 'X-Api-Key: YOUR_API_KEY' ``` Standard verification works the same way but the customer uploads documents: [Create a Verification Link](/docs/guide/customers#create-a-verification-link). ## 3. Register where the money goes For an on-ramp the customer receives crypto, so register their wallet: ```bash curl https://api.stablegate.com/v1/customers/CUSTOMER_UUID/wallets/ \ --request POST \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "rail": "ethereum", "address": "0xDC1dCb46fC96C46c4F294866AFCA81874041bB2", "name": "Max — main" }' ``` Ethereum and Polygon wallets also need an ownership signature — [External Accounts](/docs/guide/external-accounts#create-a-wallet). For an off-ramp register a bank account instead. ## 4. Check what the order will cost ```bash curl https://api.stablegate.com/v1/customers/CUSTOMER_UUID/check-fees/ \ --request POST \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "source": { "rail": "sepa", "asset": "EUR", "amount": 1000 }, "destination": { "rail": "ethereum", "asset": "USDC" } }' ``` The answer holds the commission, the amount the customer receives, the rate and how long the rate holds. Limits are a separate call — `POST /v1/customers/CUSTOMER_UUID/check-limits/` returns an empty array when the order fits. ## 5. Create the order ```bash curl https://api.stablegate.com/v1/customers/CUSTOMER_UUID/orders/ \ --request POST \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --header 'Idempotency-Key: onramp-2026-11-10-001' \ --data '{ "source": { "rail": "sepa", "asset": "EUR", "amount": 1000 }, "destination": { "rail": "ethereum", "asset": "USDC", "client_account_uuid": "WALLET_UUID" } }' ``` The response is the order: `reference` is the payment reference the customer must put on the transfer, `source.deposit_bank_info` holds the account to pay into, `state` stays `new` until the money arrives. ## 6. Follow the deposit and the payout Either poll the order or wait for the webhook. ```bash curl https://api.stablegate.com/v1/customers/CUSTOMER_UUID/orders/ORDER_UUID/ \ --header 'X-Api-Key: YOUR_API_KEY' ``` `source.deposit_status` moves to `detected` when the transfer lands, and `state` goes `new` → `processing` → `done`. Webhooks are described in [Webhooks](/docs/guide/webhooks): one event per status change, and one per deposit status change. ## What to expect and when | Step | How long it takes | |------|-------------------| | Instant verification | Minutes; longer when the screening returns a match and compliance reviews it | | SEPA deposit | Usually the same or next banking day, `sepa_instant` in seconds; `deposit_status` shows the arrival | | Payout to a wallet | Minutes after the order reaches `processing` | | Payout to a bank account | Same or next banking day | > **Note:** `Idempotency-Key` is optional but cheap: a retried request with the same key returns the first result instead of creating a second order. See [Idempotency](/docs/guide/getting-started/idempotency). --- # Customers Source: https://docs.stablegate.com/docs/guide/customers A customer represents an end-user of your platform within the Stablegate system. Every operation — orders, external accounts, fee checks, limit checks — is scoped to a customer. Before you can process any transactions, you need to register your customers with Stablegate. ## Developer Integration Path As a partner integrating with Stablegate, customer management is the first step in your processing workflow: 1. **Create customer** — register your end-user in Stablegate. 2. **Customer gets verified** — Stablegate handles KYC/compliance. 3. **Register accounts** — add bank accounts and/or crypto wallets. 4. **Process transactions** — create orders (on-ramp / off-ramp). After creating a customer, Stablegate assigns them a UUID. Verification does not start on its own: start Instant verification with the customer's name and date of birth, or request a verification link for Standard verification and hand it to the customer (see [Customer Verification](#customer-verification)). Once the customer reaches `active` status and `permissions.can_trade` is `true`, they can perform exchange operations. ## Customer Types | Type | Description | |------|-------------| | `individual` | A natural person — end-user of your platform | | `corporate` | A legal entity — company or business | > **Note:** When creating a corporate customer, the `company_name` field is required. ## Customer Statuses | Status | Description | |--------|-------------| | `new` | Customer has been registered, pending verification | | `active` | Customer is fully verified and can perform transactions | | `limited` | Customer has restricted access (e.g., pending additional verification) | | `blocked` | Customer is blocked from all operations | | `closed` | Customer account has been closed — by Stablegate, or by you with [Close a Customer](#close-a-customer). Final | --- ## Create a Customer Register a new end-user in the Stablegate system. **Endpoint:** ```text POST /v1/customers/ ``` **Request Body:** | Field | Type | Required | Description | |-------|------|----------|-------------| | `type` | string | Yes | Customer type: `individual` or `corporate` | | `email` | string | Yes | Customer's email address (must be a valid email) | | `country_code` | string | No | ISO 3166-1 alpha-2 country code (e.g., `DE`, `FR`) | | `first_name` | string | No | Customer's first name (for individuals) | | `last_name` | string | No | Customer's last name (for individuals) | | `company_name` | string | Conditional | Company name — **required** when `type` is `corporate` | | `phone` | string | No | Customer's phone number | | `external_reference` | string | No | Your internal identifier for this customer (1–255 characters). Use this to map Stablegate customers to your own system. | ### Example: Create an Individual Customer **Request:** ```bash curl https://api.stablegate.com/v1/customers/ \ --request POST \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "type": "individual", "email": "max.mustermann@example.com", "first_name": "Max", "last_name": "Mustermann", "country_code": "DE", "phone": "+491701234567", "external_reference": "USR-2025-00421" }' ``` **Response:** ```json { "uuid": "c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f", "customer_id": "CUS-000123", "external_reference": "USR-2025-00421", "username": "max.mustermann", "full_name": "Max Mustermann", "email": "max.mustermann@example.com", "phone": "+491701234567", "type": "individual", "state": "new", "country_code": "DE", "email_verified": false, "created_at": "2025-01-15T10:00:00.000Z" } ``` ### Example: Create a Corporate Customer **Request:** ```bash curl https://api.stablegate.com/v1/customers/ \ --request POST \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "type": "corporate", "email": "finance@acme-corp.com", "company_name": "ACME Corporation GmbH", "country_code": "DE", "phone": "+4930123456789", "external_reference": "CORP-2025-00089" }' ``` **Response:** ```json { "uuid": "d2e3f4a5-b6c7-8d9e-0f1a-2b3c4d5e6f7a", "customer_id": "CUS-000124", "external_reference": "CORP-2025-00089", "username": "acme-corp", "full_name": "ACME Corporation GmbH", "email": "finance@acme-corp.com", "phone": "+4930123456789", "type": "corporate", "state": "new", "country_code": "DE", "email_verified": false, "created_at": "2025-01-15T10:05:00.000Z" } ``` ### Response Fields | Field | Type | Description | |-------|------|-------------| | `uuid` | string (UUID) | Unique customer identifier — use this in all subsequent API calls | | `customer_id` | string | Human-readable customer ID assigned by the system | | `external_reference` | string \| null | Your internal reference, if provided during creation | | `username` | string | Assigned by Stablegate — the customer's email, lowercased | | `full_name` | string \| null | Full name (individual) or company name (corporate) | | `email` | string \| null | Customer's email address | | `phone` | string \| null | Customer's phone number | | `type` | string \| null | Customer type: `individual` or `corporate` | | `state` | string | Current customer status | | `country_code` | string \| null | ISO 3166-1 alpha-2 country code | | `email_verified` | boolean | Whether the customer's email has been verified | | `created_at` | string (ISO 8601) | Customer creation timestamp | > **Warning:** Store the returned `uuid` — it is required as a path parameter for all customer-scoped endpoints (orders, external accounts, fees, limits). > > **Note:** `email` must be unique among your customers, regardless of customer `type`. If one of your customers already uses the email, creation fails with `400`: > > ```json > { > "trace_id": "3fdf0457-00b3-4b87-9ed6-ff97ae1f4375", > "error_code": "devapi_email_occupied_error", > "message": "Email or username is already occupied" > } > ``` > > Match on `error_code` to tell a taken email from other request errors. An email that cannot be registered for another reason answers `404` without further detail. If the same real-world person needs both an individual and a corporate record, register the second one under a distinct email alias. --- ## Get Customer Details Retrieve the full profile of a customer, including their permissions. **Endpoint:** ```text GET /v1/customers/{customer_uuid}/ ``` **Path Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `customer_uuid` | string (UUID) | The customer's UUID | **Request:** ```bash curl https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/ \ --header 'X-Api-Key: YOUR_API_KEY' ``` **Response:** ```json { "uuid": "c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f", "customer_id": "CUS-000123", "external_reference": "USR-2025-00421", "username": "max.mustermann", "full_name": "Max Mustermann", "email": "max.mustermann@example.com", "phone": "+491701234567", "type": "individual", "state": "active", "country_code": "DE", "email_verified": true, "created_at": "2025-01-15T10:00:00.000Z", "kyc_level": { "uuid": "kl1a2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c", "legal_entity_uuid": "le1a2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c", "key": "standard", "client_type": "all", "name": "Standard", "title": "Standard KYC Level", "icon": null, "approval_date": "2025-01-16T09:00:00.000Z", "approved_by": null }, "is_funded": true, "permissions": { "can_trade": true, "can_download_aol": true } } ``` ### Additional Fields (compared to creation response) | Field | Type | Description | |-------|------|-------------| | `kyc_level` | object \| null | Approved KYC level. `kyc_level.key` is `light` (Instant), `standard`, or `advanced`; `null` until the first verification is approved | | `is_funded` | boolean | Whether the customer has made at least one deposit | | `permissions.can_trade` | boolean | Whether the customer is allowed to create exchange orders | | `permissions.can_download_aol` | boolean | Whether the account opening letter is available for download. `true` when the customer's KYC level qualifies for AOL generation. | > **Tip:** Check `permissions.can_trade` before creating orders. This flag reflects whether the customer has completed all necessary verification and onboarding steps. An `active` customer with Standard verification still has `can_trade: false` until the account is activated with a test deposit or a video interview — see [Account Activation](/docs/verification/compliance/individual/activation). Through the API the test deposit is collected by a [verification order](/docs/guide/orders#activate-an-account-with-a-verification-order). Customers with Instant verification (`kyc_level.key: "light"`) can trade without activation. --- ## List Customers Retrieve a paginated list of your customers with optional filtering and sorting. **Endpoint:** ```text GET /v1/customers/ ``` ### Query Parameters #### Pagination | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `limit` | integer | 100 | Maximum number of customers to return | | `offset` | integer | 0 | Number of customers to skip | #### Filtering | Parameter | Type | Description | |-----------|------|-------------| | `client_id` | string | Exact match on customer ID | | `email` | string | Exact match on email | | `phone` | string | Exact match on phone number | | `username` | string | Exact match on username | | `full_name` | string | Exact match on full name | | `external_reference` | string | Exact match on external reference | | `type` | string | Filter by customer type: `individual`, `corporate` | | `country_code` | string | Filter by country code | | `email_verified` | boolean | Filter by email verification status | | `status` | array | Filter by one or more statuses: `new`, `active`, `blocked`, `limited`, `closed`. Note: the query parameter is named `status`, while the corresponding response field is `state`. | | `pattern` | string | Free-text search across multiple fields | | `is_funded` | boolean | Filter by funded status | | `kyc_level_key` | array | Filter by KYC level keys | | `risk_level_key` | array | Filter by risk level keys | | `partner_uuid` | string (UUID) | Filter by partner UUID | | `partner_email` | string | Filter by partner email | | `accuity_sanctions` | boolean | Filter by sanctions screening result | | `video_verification_passed` | boolean | Filter by video verification status | #### Partial Match Filters Use `__like` suffix for substring (partial) matching: | Parameter | Description | |-----------|-------------| | `email__like` | Partial match on email | | `phone__like` | Partial match on phone | | `username__like` | Partial match on username | | `full_name__like` | Partial match on full name | | `client_id__like` | Partial match on customer ID | | `external_reference__like` | Partial match on external reference | | `partner_full_name__like` | Partial match on partner name | | `partner_email__like` | Partial match on partner email | #### Date Range Filters Use `__gte` (greater than or equal) and `__lte` (less than or equal) suffixes with dates in `YYYY-MM-DD` format: | Parameter | Description | |-----------|-------------| | `created_at__gte` | Customers created on or after this date | | `created_at__lte` | Customers created on or before this date | | `is_funded_updated_at__gte` | Funded status updated on or after this date | | `is_funded_updated_at__lte` | Funded status updated on or before this date | | `accuity_sanctions_updated_at__gte` | Sanctions check updated on or after this date | | `accuity_sanctions_updated_at__lte` | Sanctions check updated on or before this date | | `video_verification_passed_updated_at__gte` | Video verification updated on or after this date | | `video_verification_passed_updated_at__lte` | Video verification updated on or before this date | | `first_deposit_date__gte` | First deposit on or after this date | | `first_deposit_date__lte` | First deposit on or before this date | > **Note:** Date range filters also support `__gt` (strictly greater than) and `__lt` (strictly less than) suffixes. #### Sorting | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `sort` | string | `desc` | Sort direction: `asc` or `desc` | | `order_by` | string | `id` | Sort field (see available values below) | **Available `order_by` values:** `id`, `uuid`, `client_id`, `email`, `phone`, `username`, `created_at`, `full_name`, `type`, `email_verified`, `country_code`, `status`, `kyc_level_key`, `kyc_level_order`, `risk_level_key`, `risk_level_order`, `partner_full_name`, `partner_email`, `user_first_name`, `user_last_name`, `user_email`, `is_funded`, `is_funded_updated_at`, `accuity_sanctions`, `accuity_sanctions_updated_at`, `video_verification_passed`, `video_verification_passed_updated_at`, `first_deposit_date` ### Example: List Active Individual Customers **Request:** ```bash curl 'https://api.stablegate.com/v1/customers/?type=individual&status=active&limit=10&sort=desc&order_by=created_at' \ --header 'X-Api-Key: YOUR_API_KEY' ``` **Response:** ```json [ { "uuid": "c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f", "customer_id": "CUS-000123", "external_reference": "USR-2025-00421", "username": "max.mustermann", "email": "max.mustermann@example.com", "full_name": "Max Mustermann", "type": "individual", "state": "active", "kyc_level": { "uuid": "kl1a2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c", "legal_entity_uuid": "le1a2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c", "key": "standard", "client_type": "all", "name": "Standard", "title": "Standard KYC Level", "icon": null, "approval_date": "2025-01-16T09:00:00.000Z", "approved_by": null }, "phone": "+491701234567", "email_verified": true, "country_name": "Germany", "country_code": "DE", "created_at": "2025-01-15T10:00:00.000Z" } ] ``` ### Example: Search Customers by Email Pattern **Request:** ```bash curl 'https://api.stablegate.com/v1/customers/?email__like=example.com&limit=50' \ --header 'X-Api-Key: YOUR_API_KEY' ``` ### List Response Fields | Field | Type | Description | |-------|------|-------------| | `uuid` | string (UUID) | Customer UUID | | `customer_id` | string | Human-readable customer ID | | `external_reference` | string \| null | Your internal reference | | `username` | string | Customer username | | `email` | string \| null | Customer email | | `full_name` | string \| null | Full name or company name | | `type` | string \| null | Customer type | | `state` | string | Current status | | `kyc_level` | object \| null | Current KYC level details. `kyc_level.key` is `light` (Instant), `standard`, or `advanced`; `null` until the first verification is approved | | `phone` | string \| null | Phone number | | `email_verified` | boolean | Email verification status | | `country_name` | string \| null | Country display name | | `country_code` | string \| null | ISO country code | | `created_at` | string (ISO 8601) | Creation timestamp | --- ## Close a Customer Close a customer you created by mistake or for an integration test. **Endpoint:** ```text DELETE /v1/customers/{customer_uuid}/ ``` **Path Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `customer_uuid` | string (UUID) | The customer's UUID | **Request:** ```bash curl https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/ \ --request DELETE \ --header 'X-Api-Key: YOUR_API_KEY' ``` **Response:** `204 No Content`. Closing a customer that is already closed also answers `204`. A customer can be closed only while it is in the `new` state, has no KYC level, and has no submitted verification — a verification link that was created but never completed does not count. Customers past that point are closed by Stablegate support. | Status | `error_code` | Meaning | |--------|--------------|---------| | `404` | `devapi_not_found_error` | The customer does not exist or is not yours | | `409` | `devapi_customer_close_error` | The customer is not in a state that allows closing | > **Caution:** **Closing is irreversible.** The record is not deleted: it stays in `GET /v1/customers/{customer_uuid}/` and in the list with `state: "closed"`, and its email stays taken — creating a new customer with the same email fails with `devapi_email_occupied_error`. --- ## Download Account Opening Letter Download the account opening letter (AOL) for a customer as a PDF document. This letter is generated after the customer passes verification. **Endpoint:** ```text GET /v1/customers/{customer_uuid}/account-opening-letter/ ``` **Path Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `customer_uuid` | string (UUID) | The customer's UUID | **Request:** ```bash curl -X GET https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/account-opening-letter/ \ -H "X-Api-Key: YOUR_API_KEY" \ --output account_opening_letter.pdf ``` **Response:** The response is a binary PDF file with the following headers: | Header | Value | |--------|-------| | `Content-Type` | `application/pdf;charset=UTF-8` | | `Content-Disposition` | `attachment; filename*=UTF-8''account_opening_letter.pdf` | | `Content-Transfer-Encoding` | `binary` | > **Note:** The AOL is only available when `permissions.can_download_aol` is `true` in the customer detail response. Check this flag before requesting the download. --- ## Customer Verification Individual customers created through the Partner API can be verified at two levels — see [Individual Verification](/docs/verification/compliance/individual) for what each level allows: | Level | `kyc_level.key` | How to start it | What the customer does | |-------|-----------------|-----------------|------------------------| | **Instant** | `light` | [Start Instant Verification](#start-instant-verification) with the customer's name and date of birth | Nothing — Stablegate runs an automated AML screening and approves the application when the screening is clear | | **Standard** | `standard` | [Create a Verification Link](#create-a-verification-link) and redirect the customer to it | Uploads an ID document, passes a liveness check, provides proof of address, then activates the account with a test deposit or a video interview | Corporate customers are verified at the Standard level only. ### Start Instant Verification Submit the customer's identity details. Stablegate creates an Instant (`light`) KYC application, runs the AML screening (sanctions and politically exposed persons), and approves the application automatically when the screening is clear — usually within minutes. If the screening returns a match, the application goes to manual compliance review. **Endpoint:** ```text POST /v1/customers/{customer_uuid}/verification/instant/ ``` **Request Body:** | Field | Type | Required | Description | |-------|------|----------|-------------| | `first_name` | string | Yes | Customer's legal first name | | `last_name` | string | Yes | Customer's legal last name | | `date_of_birth` | string | Yes | Date of birth in `YYYY-MM-DD` format | **Request:** ```bash curl https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/verification/instant/ \ --request POST \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "first_name": "Max", "last_name": "Mustermann", "date_of_birth": "1990-05-17" }' ``` **Response:** ```json { "uuid": "ka1a2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c", "status": "waiting", "kyc_level_key": "light" } ``` | Field | Type | Description | |-------|------|-------------| | `uuid` | string | KYC application UUID | | `status` | string | Application status right after submission — `waiting` (under automated screening or compliance review) | | `kyc_level_key` | string | Always `light` | The response only confirms that the application has been submitted. To learn the outcome, poll [Check the Verification State](#check-the-verification-state): `kyc_level` becomes `{"key": "light", ...}` once the application is approved, and `permissions.can_trade` turns `true` at the same time — Instant verification needs no account activation. If compliance rejects the application, `kyc_level` stays `null`. **Errors:** | Status | `error_code` | `message` | Meaning | |--------|--------------|-----------|---------| | `400` | `devapi_instant_verification_error` | `Instant verification is not configured` | Instant verification is not enabled for your partner account. Use Standard verification (link) or contact Stablegate to enable it. | | `400` | `devapi_instant_verification_error` | `Instant verification is available for individual customers only` | The customer is a company. Use Standard verification. | | `409` | `devapi_instant_verification_error` | `Not available in current state` | The customer already has a verification application in progress or approved. Check `kyc_level` and the application status instead of resubmitting. | All three errors share the same `error_code`, so tell them apart by the HTTP status and the `message`. Calling the endpoint again for a customer whose Instant application was created but never submitted (for example, a previous call failed midway) completes that submission with the newly provided details. ### Create a Verification Link Request a hosted Standard verification flow for the customer and redirect them to the returned `url`. **Endpoint:** ```text POST /v1/customers/{customer_uuid}/verification/link/ ``` **Request:** ```bash curl https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/verification/link/ \ --request POST \ --header 'X-Api-Key: YOUR_API_KEY' ``` **Response:** ```json { "uuid": "ka1a2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c", "url": "https://in.sumsub.com/websdk/p/sbx_uni_abc123def456", "token": "_act-sbx-jwt-eyJhbGciOi..." } ``` | Field | Type | Description | |-------|------|-------------| | `uuid` | string | KYC application UUID | | `url` | string | Hosted verification page (Sumsub WebSDK) to redirect the customer to. The link is valid for **30 minutes**; request a new one if it expires | | `token` | string | Sumsub SDK access token for the same session — only needed if you embed the [Sumsub WebSDK](https://docs.sumsub.com/docs/get-started-with-web-sdk) in your own app instead of redirecting to `url` | Calling the endpoint again for a customer whose Standard application is still open — `new`, `waiting` or `pending` — returns the same application with a fresh link, so the customer can resume uploading after the previous link expires. The endpoint responds with `409` when the open application was submitted at another level, for example an Instant one, and with `409` when the customer already holds an approved KYC level. After the customer completes the flow, poll [Check the Verification State](#check-the-verification-state): `kyc_level` is set once compliance approves the application, and `permissions.can_trade` becomes `true` once the account is activated. ### Check the Verification State Returns the latest KYC application of the customer: its status, the level it was submitted for, and `message` — what compliance asked the customer for. Read the outcome here instead of inferring it from `kyc_level`. **Endpoint:** ```text GET /v1/customers/{customer_uuid}/verification/ ``` **Request:** ```bash curl https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/verification/ \ --header 'X-Api-Key: YOUR_API_KEY' ``` **Response — compliance asked the customer for another document:** ```json { "kyc_level_key": null, "application": { "uuid": "ka1a2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c", "status": "pending", "kyc_level_key": "standard", "message": "The utility bill is older than six months. Please upload a recent one.", "created_at": "2026-11-02T09:15:00.000Z", "updated_at": "2026-11-05T14:20:00.000Z", "last_status_change_at": "2026-11-05T14:20:00.000Z" } } ``` | Field | Type | Description | |-------|------|-------------| | `kyc_level_key` | string or null | Level the customer holds today. `null` until an application is approved | | `application` | object or null | The latest application of the customer, `null` when the customer never started verification | | `application.status` | string | `new`, `waiting`, `pending`, `approved`, `rejected` or `cancelled` — see below | | `application.kyc_level_key` | string | Level the application was submitted for | | `application.message` | string or null | What compliance asked the customer for. `null` when there is nothing to pass on | | `application.created_at`, `updated_at`, `last_status_change_at` | string | ISO 8601 timestamps | **What the status means:** | Status | Where the application stands | What to do | |--------|------------------------------|------------| | `new` | Created, not started | Send the customer the [verification link](#create-a-verification-link) | | `waiting` | The customer is filling the form, or the automated screening runs | Wait | | `pending` | Compliance is reviewing, or asked the customer for more documents | Read `message`, pass it to the customer, and re-issue the link if they need to upload again | | `approved` | The application passed | `kyc_level_key` is set; the customer may still need activation, see [Account Activation](/docs/verification/individual/standard-verification) | | `rejected` | The application did not pass | `kyc_level_key` stays `null`; read `message` and contact Stablegate support before starting a new application | | `cancelled` | The application was cancelled | Start a new application | A customer who never started verification answers `{"kyc_level_key": null, "application": null}`. --- ## Best Practices - **Create customers early.** Register customers as soon as they sign up on your platform and start verification right away: Instant verification is usually approved within minutes, Standard verification takes longer. - **Store the customer `uuid`.** Map it to your internal user ID for all subsequent API calls. - **Poll customer status.** After creation, the customer starts in `new` status. Poll the detail endpoint to know when they reach `active` status with `permissions.can_trade: true`. There is no customer-status webhook — [webhooks](/docs/guide/webhooks) cover order status changes only. - **Check `permissions.can_trade`** before initiating any exchange operations. - **Use `external_reference` to link customers.** Pass your internal user ID as `external_reference` during creation — it makes reconciliation between your system and Stablegate straightforward. You can also filter customers by `external_reference` to look up Stablegate records by your own identifiers. - **Use filtering for dashboards.** The list endpoint's extensive filter options make it suitable for building admin dashboards or partner management UIs. --- # Exchange Routes Source: https://docs.stablegate.com/docs/guide/exchange-routes Exchange routes define the available conversion paths between fiat currencies and digital assets. Before creating an order, you should verify that the desired route is supported and currently available. ## List All Routes Retrieve the full list of supported exchange routes. **Endpoint:** ```text GET /v1/exchange-routes/ ``` **Request:** ```bash curl https://api.stablegate.com/v1/exchange-routes/ \ --header 'X-Api-Key: YOUR_API_KEY' ``` **Response:** ```json [ { "source": { "rail": "sepa", "asset": { "code": "EUR", "fraction_digits": 2 } }, "destination": { "rail": "ethereum", "asset": { "code": "USDT", "fraction_digits": 6 } } }, { "source": { "rail": "sepa", "asset": { "code": "EUR", "fraction_digits": 2 } }, "destination": { "rail": "ethereum", "asset": { "code": "USDC", "fraction_digits": 6 } } }, { "source": { "rail": "sepa", "asset": { "code": "EUR", "fraction_digits": 2 } }, "destination": { "rail": "ethereum", "asset": { "code": "EURC", "fraction_digits": 6 } } }, { "source": { "rail": "ethereum", "asset": { "code": "USDT", "fraction_digits": 6 } }, "destination": { "rail": "sepa", "asset": { "code": "EUR", "fraction_digits": 2 } } }, { "source": { "rail": "ethereum", "asset": { "code": "USDC", "fraction_digits": 6 } }, "destination": { "rail": "sepa", "asset": { "code": "EUR", "fraction_digits": 2 } } }, { "source": { "rail": "ethereum", "asset": { "code": "EURC", "fraction_digits": 6 } }, "destination": { "rail": "sepa", "asset": { "code": "EUR", "fraction_digits": 2 } } } ] ``` ### Response Fields | Field | Type | Description | |-------|------|-------------| | `source.rail` | string | Payment rail code for the incoming side (e.g., `sepa`, `ethereum`) | | `source.asset.code` | string | Currency code for the incoming asset (e.g., `EUR`, `USDT`) | | `source.asset.fraction_digits` | integer | Maximum decimal places for amounts in this asset | | `destination.rail` | string | Payment rail code for the outgoing side | | `destination.asset.code` | string | Currency code for the outgoing asset | | `destination.asset.fraction_digits` | integer | Maximum decimal places for amounts in this asset | ### Understanding `fraction_digits` The `fraction_digits` value indicates the maximum decimal precision for amounts: - `EUR` has `fraction_digits: 2` — amounts can have up to 2 decimal places (e.g., `1000.50`) - `USDC` has `fraction_digits: 6` — amounts can have up to 6 decimal places (e.g., `1000.123456`) Always respect `fraction_digits` when specifying amounts in order creation or fee checks. ## Check Route Availability A route may exist in the system but be temporarily unavailable. Before creating an order, check that the specific route is currently operational. **Endpoint:** ```text POST /v1/exchange-routes/check/ ``` ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `source.rail` | string | Yes | Source payment rail code | | `source.asset_code` | string | Yes | Source currency code | | `destination.rail` | string | Yes | Destination payment rail code | | `destination.asset_code` | string | Yes | Destination currency code | ### Example: Route Available **Request:** ```bash curl https://api.stablegate.com/v1/exchange-routes/check/ \ --request POST \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "source": { "rail": "sepa", "asset_code": "EUR" }, "destination": { "rail": "ethereum", "asset_code": "USDC" } }' ``` **Response:** ```json { "available": true, "route": { "source": { "rail": "sepa", "asset": { "code": "EUR", "fraction_digits": 2 } }, "destination": { "rail": "ethereum", "asset": { "code": "USDC", "fraction_digits": 6 } } } } ``` ### Example: Route Unavailable **Request:** ```bash curl https://api.stablegate.com/v1/exchange-routes/check/ \ --request POST \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "source": { "rail": "swift", "asset_code": "USD" }, "destination": { "rail": "ethereum", "asset_code": "USDC" } }' ``` **Response:** ```json { "available": false, "route": null } ``` ### Response Fields | Field | Type | Description | |-------|------|-------------| | `available` | boolean | `true` if the route is currently operational, `false` otherwise | | `route` | object \| null | Route details when available, `null` when not | ## Best Practices - **Cache routes locally** but refresh periodically. Routes don't change frequently, but availability can. - **Always check availability** before creating an order to avoid unnecessary errors. - **Use the check endpoint** rather than filtering the list response — it validates the full route configuration. ## Rail Codes Reference | Code | Type | Description | |------|------|-------------| | `sepa` | Bank | SEPA — European bank transfers in EUR | | `swift` | Bank | SWIFT — International bank transfers | | `ach` | Bank | ACH — US domestic bank transfers | | `ethereum` | Crypto | Ethereum mainnet | | `polygon` | Crypto | Polygon (MATIC) network | | `tron` | Crypto | Tron (TRX) network | | `algorand` | Crypto | Algorand network | | `ripple` | Crypto | Ripple (XRP) network | | `stellar` | Crypto | Stellar network | | `xinfin` | Crypto | XinFin (XDC) network | > **Note:** Not all rails are active for exchange routes. Query the API for the current list of supported combinations. ## Asset Codes Reference | Code | Type | Description | |------|------|-------------| | `EUR` | Fiat | Euro | | `USD` | Fiat | US Dollar | | `GBP` | Fiat | British Pound | | `CHF` | Fiat | Swiss Franc | | `USDT` | Stablecoin | Tether USD | | `USDC` | Stablecoin | USD Coin | | `EURC` | Stablecoin | Euro Coin | | `EURS` | Stablecoin | STASIS Euro | > **Note:** Not all assets are available on all rails. The exchange routes endpoint returns the exact supported combinations. --- # Bank Providers Source: https://docs.stablegate.com/docs/guide/bank-providers Bank providers represent the financial institutions available for fiat operations. Use this endpoint to discover which banks are available, what payment rails they support, and which exchange routes are accessible through each bank. This information is useful for: - Displaying available payment methods in your UI - Validating bank account details before registration - Showing customers which currencies and rails are available ## List Bank Providers Retrieve all active bank providers with their supported rails and exchange routes. **Endpoint:** ```text GET /v1/bank-providers/ ``` ### Query Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `rail` | string | No | Filter by rail code (e.g., `sepa`, `swift`) | | `country` | string | No | Filter by bank country (ISO 3166-1 alpha-3, e.g., `CHE`, `LTU`) | ### Example: List All Bank Providers **Request:** ```bash curl https://api.stablegate.com/v1/bank-providers/ \ --header 'X-Api-Key: YOUR_API_KEY' ``` **Response:** ```json [ { "uuid": "d762a0e6-24ef-4962-a658-bb5714cd9d58", "name": "Zen", "bank_info": { "bank_name": "UAB ZEN.COM", "bank_code": "BZENLT22", "bank_country": "LTU" }, "rails": [ { "code": "sepa", "name": "SEPA", "type": "bank" } ], "exchange_routes": [ { "source_asset": "EUR", "destination_asset": "USDC" }, { "source_asset": "EUR", "destination_asset": "USDT" }, { "source_asset": "USDC", "destination_asset": "EUR" }, { "source_asset": "USDT", "destination_asset": "EUR" } ] }, { "uuid": "542148ce-0299-4bb0-8b8c-c2e37fa65ece", "name": "Bivial", "bank_info": { "bank_name": "Bivial AG", "bank_code": "KLARCH22", "bank_country": "CHE", "bank_address": "Industriestrasse 24, 6300 Zug, Switzerland" }, "rails": [ { "code": "sepa", "name": "SEPA", "type": "bank" } ], "exchange_routes": [ { "source_asset": "EUR", "destination_asset": "USDC" }, { "source_asset": "CHF", "destination_asset": "USDC" }, { "source_asset": "USDC", "destination_asset": "EUR" }, { "source_asset": "USDC", "destination_asset": "CHF" } ] } ] ``` ### Example: Filter by Rail **Request:** ```bash curl 'https://api.stablegate.com/v1/bank-providers/?rail=sepa' \ --header 'X-Api-Key: YOUR_API_KEY' ``` ### Example: Filter by Country **Request:** ```bash curl 'https://api.stablegate.com/v1/bank-providers/?country=CHE' \ --header 'X-Api-Key: YOUR_API_KEY' ``` ### Response Fields | Field | Type | Description | |-------|------|-------------| | `uuid` | string | Unique identifier of the bank provider | | `name` | string | Display name of the bank provider | | `bank_info` | object \| null | Banking details of the provider | | `bank_info.bank_name` | string | Official bank name | | `bank_info.bank_code` | string | Bank identifier code (BIC/SWIFT) | | `bank_info.bank_country` | string | Country code (ISO 3166-1 alpha-3) | | `bank_info.bank_address` | string \| null | Physical address of the bank | | `rails` | array | List of active payment rails supported by this bank | | `rails[].code` | string | Rail code (e.g., `sepa`, `swift`) | | `rails[].name` | string | Human-readable rail name | | `rails[].type` | string | Rail type (`bank`) | | `exchange_routes` | array | Exchange routes accessible through this bank's assets | | `exchange_routes[].source_asset` | string | Source asset code of the exchange route | | `exchange_routes[].destination_asset` | string | Destination asset code of the exchange route | > **Note:** Only active bank providers with active rails and assets are returned. A bank provider appears in the list only if it has at least one active rail with at least one active asset configured. ## Best Practices - **Cache the response** — bank provider data changes infrequently. Refresh periodically (e.g., daily). - **Use `exchange_routes`** to show customers which conversions are available through a specific bank. - **Combine with [Exchange Routes](/docs/guide/exchange-routes)** for full route details including `fraction_digits`. - **Filter by rail** when building UI dropdowns for specific payment methods (e.g., only SEPA banks). --- # External Accounts Source: https://docs.stablegate.com/docs/guide/external-accounts Before creating exchange orders, you must register your customer's external accounts — the bank accounts and crypto wallets that will serve as the source or destination of funds. - **Bank accounts** — used for fiat transfers via SEPA, SWIFT, ACH and FedWire - **Crypto wallets** — used for digital asset transfers on blockchain networks ## Bank Accounts ### Create a Bank Account Register a customer's bank account for the payment scheme the money moves on. One record describes one scheme: a SEPA account cannot carry ACH or FedWire details, and a US account cannot be used for SEPA. The number of bank accounts and wallets a customer can have is set by your account configuration; when the cap is reached the API answers `400` with the reason in `message`. **Endpoint:** ```text POST /v1/customers/{customer_uuid}/bank-accounts/ ``` **Request Body:** | Field | Type | Required | Description | |-------|------|----------|-------------| | `supported_rails` | string[] | No | Payment schemes this account is used for: `sepa`, `swift`, `ach`, `wire` (FedWire). Omitted or `null` means `sepa`. SEPA takes a separate record from ACH and FedWire | | `iban` | string | Conditional | Customer's IBAN for SEPA, spaces allowed. For ACH, FedWire and SWIFT send `account_number` | | `account_number` | string | Conditional | Beneficiary account number for ACH, FedWire and SWIFT | | `holder_name` | string | Yes | Account holder name. It must match the bank account | | `bank_country` | string | Conditional | ISO 3166-1 alpha-2 country of the bank. Required for ACH, FedWire and US SWIFT, where it must be `US`. For SEPA it is taken from the IBAN | | `bank_code` | string | Conditional | Bank BIC. Required for SWIFT; for SEPA derived from the IBAN when omitted or blank. Not used for ACH and FedWire | | `bank_routing_number` | string | Conditional | 9-digit ABA routing number. Required for US payments: ACH, FedWire and US SWIFT | | `bank_name` | string | No | Bank name. For SEPA derived from the IBAN when omitted or blank | | `bank_address` | string | No | Bank address | | `bank_city` | string | No | Bank city | | `bank_postal_code` | string | No | Bank postal code | | `bank_region` | string | No | Bank state or region | | `holder_country` | string | No | Account holder country | | `holder_city` | string | No | Account holder city | | `holder_street` | string | No | Account holder street and house number | | `holder_postal_code` | string | No | Account holder postal code | | `holder_region` | string | No | Account holder state or region | | `name` | string | No | Friendly label for the account (e.g., "Main EUR Account") | | `is_third_party` | boolean | No | `true` when the account belongs to someone other than the customer, for example a supplier. Defaults to `false` | | `receiver_type` | string | Conditional | `individual` or `company`: who owns a third-party account. Required when `is_third_party` is `true`, must be omitted otherwise | **What to send for each payment scheme:** | Scheme | `supported_rails` | Account | Bank | Holder | |--------|-------------------|---------|------|--------| | SEPA (EUR) | `["sepa"]` or omitted | `iban` | `bank_code` and `bank_name` optional, `bank_country` from the IBAN | `holder_name` | | SWIFT (non-US) | `["swift"]` | `account_number` | `bank_code`, `bank_country`, `bank_name`, `bank_address`, `bank_city`, `bank_postal_code` | `holder_name`, `holder_country`, `holder_city`, `holder_street`, `holder_postal_code` | | SWIFT (US) | `["swift"]` | `account_number`, numeric | `bank_code` with a `US` country code, `bank_country: "US"`, `bank_routing_number`, address fields | `holder_name`, `holder_country`, `holder_city`, `holder_street`, `holder_postal_code`, `holder_region` | | ACH (USD) | `["ach"]` | `account_number`, numeric | `bank_country: "US"`, `bank_routing_number`, `bank_name` | `holder_name`, `holder_country`, `holder_city`, `holder_street`, `holder_postal_code` | | FedWire (USD) | `["wire"]` | `account_number`, numeric | `bank_country: "US"`, `bank_routing_number`, `bank_name` | `holder_name`, `holder_country`, `holder_city`, `holder_street`, `holder_postal_code` | **Third-party accounts.** Mark the account as someone else's when it is not the customer's own: send `is_third_party: true` and `receiver_type`. Payouts to such an account above the invoice threshold ask for a supporting document — the order reports it with `third_party_document_required`; see [Third-Party Payouts and the Invoice](/docs/guide/orders#third-party-payouts-and-the-invoice). An account marked as the customer's own, which is the default, must carry the customer's name as the holder exactly: any other name is rejected with `400`, so register such an account as third-party. > **Warning:** The bank decides which details it needs to execute a payout. The address fields are optional in the API, but for SWIFT and US payouts the bank can require the holder's address and the bank's address. A payout without them is rejected when the order is executed, and the order fails. Send them when you register the account. **Request:** SEPA account ```bash curl https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/bank-accounts/ \ --request POST \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "iban": "DE89370400440532013000", "holder_name": "Max Mustermann", "name": "Main EUR Account", "bank_name": "Commerzbank", "bank_code": "COBADEFFXXX" }' ``` **Request:** SWIFT account ```bash curl https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/bank-accounts/ \ --request POST \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "supported_rails": [ "swift" ], "account_number": "001234567890", "holder_name": "John Smith", "holder_country": "GB", "holder_city": "London", "holder_street": "10 Downing Street", "holder_postal_code": "SW1A 2AA", "bank_country": "GB", "bank_code": "BARCGB22", "bank_name": "Barclays Bank", "bank_address": "1 Churchill Place", "bank_city": "London", "bank_postal_code": "E14 5HP", "name": "GBP account" }' ``` **Request:** US account for ACH ```bash curl https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/bank-accounts/ \ --request POST \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "supported_rails": [ "ach" ], "account_number": "000123456789", "holder_name": "John Smith", "holder_country": "US", "holder_city": "New York", "holder_street": "350 Fifth Avenue", "holder_postal_code": "10118", "bank_country": "US", "bank_routing_number": "021000021", "bank_name": "JPMorgan Chase", "name": "USD account" }' ``` **Response:** ```json { "uuid": "ba1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e", "name": "Main EUR Account", "iban": "DE89370400440532013000", "account_number": null, "supported_rails": ["sepa"], "bank_name": "Commerzbank", "bank_code": "COBADEFFXXX", "bank_country": "DE", "bank_address": null, "bank_city": null, "bank_postal_code": null, "bank_region": null, "bank_routing_number": null, "holder_name": "Max Mustermann", "holder_country": null, "holder_city": null, "holder_street": null, "holder_postal_code": null, "holder_region": null, "created_at": "2025-01-15T10:30:00.000Z", "updated_at": "2025-01-15T10:30:00.000Z" } ``` For a SEPA account `bank_code` and `bank_name` are filled from an offline bank registry, so most consumer flows need only the IBAN and the holder name. When the registry has no BIC for the IBAN — common for EMIs and fintechs — the request fails and you pass `bank_code` yourself: ```json { "trace_id": "de104ee1-da4e-4eeb-a5e6-aded99b3f417", "error_code": "devapi_bank_code_required_error", "message": "Bank code (BIC) is not available for this IBAN, pass bank_code explicitly" } ``` Use [Look Up a Bank by IBAN](#look-up-a-bank-by-iban) to check in advance what the registry knows. ### Look Up a Bank by IBAN Returns the BIC, bank name and country the registry holds for an IBAN — the same values that are filled in when you create a bank account without `bank_code`. **Endpoint:** ```text GET /v1/bank-accounts/info-by-iban/?iban={iban} ``` **Query Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `iban` | string | Yes | IBAN to look up, spaces allowed | **Request:** ```bash curl 'https://api.stablegate.com/v1/bank-accounts/info-by-iban/?iban=DE89370400440532013000' \ --header 'X-Api-Key: YOUR_API_KEY' ``` **Response:** ```json { "bank_name": "Commerzbank", "bank_code": "COBADEFFXXX", "bank_country": "DE" } ``` | Field | Type | Description | |-------|------|-------------| | `bank_name` | string \| null | Bank name; `null` when the registry has no usable entry for the IBAN | | `bank_code` | string \| null | Bank BIC; `null` when the registry has no entry for the IBAN | | `bank_country` | string \| null | ISO 3166-1 alpha-2 country code taken from the IBAN | An IBAN that cannot be parsed answers `400` with `error_code: devapi_iban_parse_error`. ### List Bank Accounts Retrieve all registered bank accounts for a customer. **Endpoint:** ```text GET /v1/customers/{customer_uuid}/bank-accounts/ ``` **Request:** ```bash curl https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/bank-accounts/ \ --header 'X-Api-Key: YOUR_API_KEY' ``` **Response:** ```json [ { "uuid": "ba1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e", "name": "Main EUR Account", "iban": "DE89370400440532013000", "account_number": null, "supported_rails": ["sepa"], "bank_name": "Commerzbank", "bank_code": "COBADEFFXXX", "bank_country": "DE", "bank_address": null, "bank_city": null, "bank_postal_code": null, "bank_region": null, "bank_routing_number": null, "holder_name": "Max Mustermann", "holder_country": null, "holder_city": null, "holder_street": null, "holder_postal_code": null, "holder_region": null, "created_at": "2025-01-15T10:30:00.000Z", "updated_at": "2025-01-15T10:30:00.000Z" } ] ``` ### Get a Bank Account Retrieve a specific bank account by its UUID. **Endpoint:** ```text GET /v1/customers/{customer_uuid}/bank-accounts/{account_uuid}/ ``` **Request:** ```bash curl https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/bank-accounts/ba1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e/ \ --header 'X-Api-Key: YOUR_API_KEY' ``` ### Update a Bank Account Replace the details of an existing bank account. The request body is the same as for creating an account, with the same per-scheme rules: send the complete set of fields, because a field you leave out is cleared rather than kept. The third-party fields are the exception: leave `is_third_party` and `receiver_type` out to keep the stored values, or send them to change the flag. **Endpoint:** ```text PUT /v1/customers/{customer_uuid}/bank-accounts/{account_uuid}/ ``` **Request:** SEPA account ```bash curl https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/bank-accounts/ba1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e/ \ --request PUT \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "iban": "DE89370400440532013000", "holder_name": "Max Mustermann", "name": "Primary EUR Account", "bank_name": "Commerzbank", "bank_code": "COBADEFFXXX" }' ``` **Request:** SWIFT account with the holder address ```bash curl https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/bank-accounts/ba1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e/ \ --request PUT \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "supported_rails": [ "swift" ], "account_number": "001234567890", "holder_name": "John Smith", "holder_country": "GB", "holder_city": "London", "holder_street": "10 Downing Street", "holder_postal_code": "SW1A 2AA", "bank_country": "GB", "bank_code": "BARCGB22", "bank_name": "Barclays Bank", "bank_address": "1 Churchill Place", "bank_city": "London", "bank_postal_code": "E14 5HP", "name": "GBP account" }' ``` ### Delete a Bank Account Remove a registered bank account. **Endpoint:** ```text DELETE /v1/customers/{customer_uuid}/bank-accounts/{account_uuid}/ ``` **Request:** ```bash curl https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/bank-accounts/ba1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e/ \ --request DELETE \ --header 'X-Api-Key: YOUR_API_KEY' ``` **Response:** `204 No Content` --- ## Crypto Wallets ### Create a Wallet Register a customer's crypto wallet for use in on-ramp (receiving crypto) or off-ramp (sending crypto) orders. **Endpoint:** ```text POST /v1/customers/{customer_uuid}/wallets/ ``` **Request Body:** | Field | Type | Required | Description | |-------|------|----------|-------------| | `rail` | string | Yes | Blockchain network: `ethereum`, `polygon`, `algorand`, `ripple`, `stellar`, `xinfin` | | `address` | string | Yes | Wallet address on the specified network | | `name` | string | No | Friendly label for the wallet | | `wallet_message` | string | Conditional | Hex-encoded ownership confirmation message. Required for Ethereum and Polygon. | | `wallet_signature` | string | Conditional | Signed ownership confirmation message. Required for Ethereum and Polygon. | A wallet address is unique per network across all Stablegate customers, not only among your own. If the address is already linked to any customer, the request is rejected before the ownership proof (`wallet_message` and `wallet_signature`) is checked, so a valid signature does not help — ask the customer for another wallet. | Status | `error_code` | `message` | |--------|--------------|-----------| | `400` | `oa_client_external_account_address_exists` | `This wallet is already linked to another account` | ### Wallet Ownership Verification (Ethereum / Polygon) For wallets on Ethereum and Polygon, you must provide proof of ownership by signing a confirmation message. **Step 1:** Construct the confirmation message: ```text I confirm the ownership of address {wallet_address}. Signature time: {ISO_8601_timestamp}. ``` **Step 2:** Sign the message with the wallet's private key and convert the message to hex. > **Warning:** The signature timestamp must not be older than 24 hours (UTC). **JavaScript example:** ```javascript import { ethers } from "ethers"; function stringToHex(str) { let hex = ""; for (let i = 0; i < str.length; i++) { hex += str.charCodeAt(i).toString(16); } return `0x${hex}`; } const wallet = new ethers.Wallet("YOUR_PRIVATE_KEY"); const ts = new Date().toISOString(); const message = `I confirm the ownership of address ${wallet.address}. Signature time: ${ts}.`; const wallet_signature = await wallet.signMessage(message); const wallet_message = stringToHex(message); // Use wallet_message and wallet_signature in the API request ``` ### Example: Create Ethereum Wallet (with ownership proof) **Request:** ```bash curl https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/wallets/ \ --request POST \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "rail": "ethereum", "address": "0xDC1dCb46fC96C46c4F294866AFCA81874041bB2", "name": "Customer ETH Wallet", "wallet_message": "0x4920636f6e6669726d20746865206f776e657273686970206f662061646472657373203078444331644362343666433936433436633446323934383636364146434138313837343034316242322e205369676e61747572652074696d653a20323032352d30312d31355431303a33303a30302e3030305a2e", "wallet_signature": "0x861818884bbf61f8d83bc3052e6c513f939512852fc92067dde5845b392248c949812e44366158d91d73ac0e9588c56dfb1d59549c3cbad52cfe4884fdc15c361" }' ``` **Response:** ```json { "uuid": "wa1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d", "name": "Customer ETH Wallet", "rail": "ethereum", "address": "0xDC1dCb46fC96C46c4F294866AFCA81874041bB2", "status": "active", "verified_at": null, "created_at": "2025-01-15T10:35:00.000Z", "updated_at": "2025-01-15T10:35:00.000Z" } ``` ### Response Fields | Field | Type | Description | |-------|------|-------------| | `status` | string | Account status: `active` or `sanctioned` | | `verified_at` | string \| null | Set once the Satoshi Test (or WalletConnect) ownership proof has been confirmed on-chain. `null` until verification completes — poll this field instead of assuming success right after the micro-transaction is sent. | ### Example: Create Wallet on Other Networks For networks that do not require ownership verification (e.g., Algorand, Ripple, Stellar, XinFin), you can create a wallet without `wallet_message` and `wallet_signature`: **Request:** ```bash curl https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/wallets/ \ --request POST \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "rail": "stellar", "address": "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", "name": "Customer Stellar Wallet" }' ``` ### List Wallets Retrieve all registered wallets for a customer. **Endpoint:** ```text GET /v1/customers/{customer_uuid}/wallets/ ``` **Request:** ```bash curl https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/wallets/ \ --header 'X-Api-Key: YOUR_API_KEY' ``` **Response:** ```json [ { "uuid": "wa1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d", "name": "Customer ETH Wallet", "rail": "ethereum", "address": "0xDC1dCb46fC96C46c4F294866AFCA81874041bB2", "status": "active", "verified_at": "2026-08-21T12:09:40.000Z", "created_at": "2025-01-15T10:35:00.000Z", "updated_at": "2025-01-15T10:35:00.000Z" } ] ``` ### Get a Wallet ```text GET /v1/customers/{customer_uuid}/wallets/{wallet_uuid}/ ``` ### Update a Wallet ```text PUT /v1/customers/{customer_uuid}/wallets/{wallet_uuid}/ ``` The request body is the same as the create endpoint. For Ethereum and Polygon wallets, new `wallet_message` and `wallet_signature` must be provided. ### Delete a Wallet ```text DELETE /v1/customers/{customer_uuid}/wallets/{wallet_uuid}/ ``` **Response:** `204 No Content` --- ## Satoshi Test The Satoshi Test endpoint returns an operational wallet address that can be used for wallet ownership verification via a micro-transaction. **Endpoint:** ```text GET /v1/customers/{customer_uuid}/wallets/satoshitest/?rail={crypto_rail} ``` **Query Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `rail` | string | Yes | Crypto rail code: `ethereum`, `polygon`, `algorand`, `ripple`, `stellar`, `xinfin` | **Request:** ```bash curl 'https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/wallets/satoshitest/?rail=ethereum' \ --header 'X-Api-Key: YOUR_API_KEY' ``` **Response:** ```json { "address": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18", "segwit_address": null } ``` ### Response Fields | Field | Type | Description | |-------|------|-------------| | `address` | string | Operational wallet address to send the micro-transaction to | | `segwit_address` | string \| null | SegWit address, if applicable for the network | --- # Fees & Limits Source: https://docs.stablegate.com/docs/guide/fees-and-limits This section covers the two pre-order checks you should perform before creating an exchange order: - **Fees** — Calculate the exact cost breakdown for a transaction so your customer sees accurate amounts before confirming. - **Limits** — Verify that the intended operation falls within the customer's transaction limits to avoid rejected orders. Both checks accept the same route and amount parameters used in order creation, making it easy to integrate them into your pre-order flow. --- # Fees Source: https://docs.stablegate.com/docs/guide/fees-and-limits/fees Onboarding — €0 Account maintenance — €0 Pay only per transaction. No hidden fees. ## Fiat ↔ Crypto Conversion Fees ### EUR | **Total volume** | **<€200,000** | **€200,000+** | **€500,000+** | | -------------------- | ------------- | ------------- | ------------- | | Standard-risk client | 1.5% | 1.0% | 0.8% | | High-risk client | 2.0% | 2.0% | 1.8% | ### USD, AED, other currencies | **Total volume** | **<$200,000** | **$200,000+** | **$500,000+** | | -------------------- | ------------- | ------------- | ------------- | | Standard-risk client | 1.5% | 1.2% | 1.0% | | High-risk client | 2.2% | 2.2% | 2.0% | ## Minimum Commission Each transaction is charged **whichever is greater: the percentage rate above, or a flat minimum commission of €20** (or currency equivalent). The €20 minimum is the platform default; a customer may have a custom minimum configured. Like the minimum transaction amount, it does not depend on the verification level — [Check Fees](#check-fees-api) returns the exact commission for the customer. This means the commission looks flat at small transaction sizes — the €20 minimum dominates until the percentage rate would itself exceed €20 — and becomes purely percentage-based above that crossover point. At the standard EUR rate (1.5%), the crossover is around €1,334: | Amount | 1.5% | Commission charged | | --------- | ------ | ------------------- | | €50 | €0.75 | €20.00 (minimum) | | €100 | €1.50 | €20.00 (minimum) | | €500 | €7.50 | €20.00 (minimum) | | €1,000 | €15.00 | €20.00 (minimum) | | €5,000 | €75.00 | €75.00 (percentage) | ## Banking Rail Fees Charged per transaction, in addition to Conversion fees | **SEPA** | €0 | | ---------------- | ------- | | **SEPA Instant** | €0 | | **ACH** | $50 | | **Fedwire** | $50 | | **Swift** | €99/$99 | ## Fee Disclosure * **High-risk fees** are approximate and are subject to personal fees for each high-risk client case. * **Net Settlement** — fees are automatically deducted from the transaction amount before confirmation. * **Developer markup** — developers/partners may apply a custom per-transaction markup. This charge is assessed in addition to Stablegate fees. * **Reporting & Audit** — full fee data available via `/transactions` and `/reports` endpoints for reconciliation and accounting. * **Availability** — not all rails and currencies listed above are currently enabled. Always query the [Exchange Routes API](/docs/guide/exchange-routes) to confirm available conversion paths. --- ## Check Fees API Before creating an order, use the fee check endpoint to calculate the exact costs for your customer. This allows you to display accurate fee breakdowns in your application before the customer confirms the transaction. ### Endpoint Calculate fees for a specific exchange operation. ```text POST /v1/customers/{customer_uuid}/check-fees/ ``` ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `source.rail` | string | Yes | Source payment rail (e.g., `sepa`, `ethereum`) | | `source.asset` | string | Yes | Source asset code (e.g., `EUR`, `USDT`) | | `source.amount` | number \| string | Conditional | Amount on the source side. Provide either `source.amount` or `destination.amount`. | | `destination.rail` | string | Yes | Destination payment rail | | `destination.asset` | string | Yes | Destination asset code | | `destination.amount` | number \| string | Conditional | Amount on the destination side. Provide either `source.amount` or `destination.amount`. | > **Note:** Specify the amount on the side you know. For example, if the customer wants to send 1,000 EUR, set `source.amount`. If they want to receive exactly 1,000 USDC, set `destination.amount`. The API will calculate the other side. ### Example: On-Ramp Fee Check (source amount specified) The customer wants to convert 1,000 EUR to USDC. **Request:** ```bash curl https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/check-fees/ \ --request POST \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "source": { "rail": "sepa", "asset": "EUR", "amount": 1000 }, "destination": { "rail": "ethereum", "asset": "USDC" } }' ``` **Response:** ```json { "source": { "rail": "sepa", "asset": "EUR", "amount": "1000.00" }, "destination": { "rail": "ethereum", "asset": "USDC", "amount": "978.50" }, "commission": { "asset": "EUR", "amount": "15.00" }, "exchange_rate": "1.006700" } ``` In this example: * The customer sends **1,000.00 EUR** * The fee is **15.00 EUR** * The exchange rate is **1.0067** (EUR/USDC) * The customer receives **978.50 USDC** (after fees and conversion) ### Example: Off-Ramp Fee Check (source amount specified) The customer wants to convert 500 USDT to EUR. **Request:** ```bash curl https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/check-fees/ \ --request POST \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "source": { "rail": "ethereum", "asset": "USDT", "amount": 500 }, "destination": { "rail": "sepa", "asset": "EUR" } }' ``` **Response:** ```json { "source": { "rail": "ethereum", "asset": "USDT", "amount": "500.000000" }, "destination": { "rail": "sepa", "asset": "EUR", "amount": "473.25" }, "commission": { "asset": "USDT", "amount": "10.00" }, "exchange_rate": "0.966500" } ``` ### Response Fields | Field | Type | Description | |-------|------|-------------| | `source.rail` | string | Source payment rail | | `source.asset` | string | Source asset code | | `source.amount` | string | Total amount to be debited from the source | | `destination.rail` | string | Destination payment rail | | `destination.asset` | string | Destination asset code | | `destination.amount` | string | Net amount to be credited at the destination | | `commission.asset` | string | Asset the commission is charged in: the source asset, so on an off-ramp the crypto the customer sends | | `commission.amount` | string | Fee amount | | `exchange_rate` | string \| null | Indicative exchange rate used for conversion, with the exchange spread included: it is the rate `destination.amount` is calculated with. May be `null` for same-currency operations. | > **Note:** All amounts in the response are returned as strings to preserve decimal precision. Each amount is rounded to the precision of its asset, so crypto amounts can carry more than two decimals. > > **Important:** `check-fees` is a preview. The `exchange_rate` is calculated at the time of the request and is not reserved: creating an order calculates it again, and an unfunded order is re-priced until the deposit arrives. The rate an order currently holds, and until when, are in `exchange_rate` and `rate_valid_until` of the [order details](/docs/guide/orders#response-fields). ## Best Practices * **Check fees immediately before order creation** to ensure the customer sees the most accurate quote. Exchange rates and fees can fluctuate. * **Display the full breakdown** to your customers: source amount, fee, and destination amount. * **Handle both directions**: some customers think in terms of "I want to send X," others in terms of "I want to receive Y." Use `source.amount` or `destination.amount` accordingly. --- # Limits Source: https://docs.stablegate.com/docs/guide/fees-and-limits/limits **Minimum transaction:** a platform default, or a custom value configured for the customer. It does not depend on the verification level — read the exact value from the [Limits API](#get-customer-limits) (`operation_min_amount` rule). **Limits by verification level:** * **No verification** — **€0**. Customers cannot transact at all before completing at least Instant Verification; this is intentional, not a bug. * **Instant (Light) verification** (`kyc_level_key: "light"`) — **€1,000 per month** (aggregate turnover). * **Standard verification** (`kyc_level_key: "standard"`) — **€30,000** lifetime; a customer with a higher-risk compliance profile gets **€15,000**. Higher personal limits are available following Source-of-Funds (SoF) review. See [Individual Verification](/docs/verification/compliance/individual) for what each level requires. --- ## Limits API Customers have transaction limits based on their compliance profile. Before creating an order, verify that the intended operation falls within the customer's limits to avoid rejected transactions. ### Get Customer Limits Retrieve all active limits for a customer. **Endpoint:** ```text GET /v1/customers/{customer_uuid}/limits/ ``` **Request:** ```bash curl https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/limits/ \ --header 'X-Api-Key: YOUR_API_KEY' ``` **Response:** ```json [ { "uuid": "lm1a2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c", "kyc_level_key": "standard", "risk_level_key": "low", "limit_amount": "15000.00", "limit_rule": { "uuid": "lr1a2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c", "name": "Monthly Transaction Limit", "scope": "period", "period": "month", "rail_types": ["bank", "crypto"], "rail_codes": null, "operation_types": ["funding"], "asset_codes": null }, "is_overridden": false, "is_custom_limit": false, "used_amount": "3500.00", "available_amount": "11500.00" }, { "uuid": "lm2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d", "kyc_level_key": "standard", "risk_level_key": "low", "limit_amount": "5000.00", "limit_rule": { "uuid": "lr2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d", "name": "Single Transaction Limit", "scope": "operation", "period": null, "rail_types": ["bank", "crypto"], "rail_codes": null, "operation_types": ["funding"], "asset_codes": null }, "is_overridden": false, "is_custom_limit": false, "used_amount": null, "available_amount": null }, { "uuid": "lm3c4d5e-6f7a-8b9c-0d1e-2f3a4b5c6d7e", "kyc_level_key": "standard", "risk_level_key": "low", "limit_amount": "50.00", "limit_rule": { "uuid": "lr3c4d5e-6f7a-8b9c-0d1e-2f3a4b5c6d7e", "name": "Minimum Transaction Amount", "scope": "operation_min_amount", "period": null, "rail_types": ["bank"], "rail_codes": null, "operation_types": ["funding"], "asset_codes": ["EUR"] }, "is_overridden": false, "is_custom_limit": false, "used_amount": null, "available_amount": null } ] ``` ### Response Fields | Field | Type | Description | |-------|------|-------------| | `uuid` | string | Limit UUID | | `kyc_level_key` | string \| null | Customer's KYC level | | `risk_level_key` | string \| null | Customer's risk level | | `limit_amount` | string | Maximum (or minimum) allowed amount, a decimal string | | `limit_rule` | object | Rule that defines the limit (see below) | | `is_overridden` | boolean | Whether this limit has been overridden from the default | | `is_custom_limit` | boolean | Whether this is a custom limit specific to this customer | | `used_amount` | string \| null | Amount already used within the current period (for periodic limits), a decimal string | | `available_amount` | string \| null | Remaining amount available (for periodic limits), a decimal string | All amounts are EUR decimal strings with two fractional digits (`"100.00"`, `"12.50"`), the same as amounts in orders and `check-fees`. Parse them with a decimal type, not a float. ### Limit Rule Fields | Field | Type | Description | |-------|------|-------------| | `uuid` | string | Limit rule UUID | | `name` | string | Human-readable name of the limit rule | | `scope` | string | Limit scope (see below) | | `period` | string \| null | Time period for periodic limits | | `rail_types` | array \| null | Rail types this limit applies to (e.g., `["bank", "crypto"]`) | | `rail_codes` | array \| null | Specific rail codes this limit applies to | | `operation_types` | array \| null | Side of the order this limit applies to: `funding` (the incoming leg) and/or `payout` (the outgoing leg). `null` means both | | `asset_codes` | array \| null | Specific asset codes this limit applies to | ### Limit Scopes | Scope | Description | |-------|-------------| | `operation` | Maximum amount per single transaction | | `period` | Cumulative maximum over a time period | | `operation_min_amount` | Minimum amount per single transaction | ### Limit Periods | Period | Description | |--------|-------------| | `day` | Daily rolling period | | `month` | Monthly rolling period | | `quarter` | Quarterly rolling period | | `year` | Yearly rolling period | | `lifetime` | Total lifetime limit | --- ### Check Limits for a Specific Operation Validate whether a planned operation falls within the customer's limits. This is more targeted than listing all limits — it evaluates the specific route against all applicable limit rules and returns **only the rules the operation breaks**. An empty list means the operation is within limits; every item in the response is a reason the order would be refused. **Endpoint:** ```text POST /v1/customers/{customer_uuid}/check-limits/ ``` #### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `source.rail` | string | Yes | Source payment rail | | `source.asset` | string | Yes | Source asset code | | `source.amount` | number \| string | No | Source amount (if known) | | `destination.rail` | string | Yes | Destination payment rail | | `destination.asset` | string | Yes | Destination asset code | | `destination.amount` | number \| string | No | Destination amount (if known) | #### Example: Within Limits **Request:** ```bash curl https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/check-limits/ \ --request POST \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "source": { "rail": "sepa", "asset": "EUR", "amount": 1000 }, "destination": { "rail": "ethereum", "asset": "USDC" } }' ``` **Response:** ```json [] ``` No rule is broken — an order for this amount passes the limit check. #### Example: Limit Exceeded **Request:** ```bash curl https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/check-limits/ \ --request POST \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "source": { "rail": "sepa", "asset": "EUR", "amount": 7000 }, "destination": { "rail": "ethereum", "asset": "USDC" } }' ``` **Response:** ```json [ { "limit_rule": { "uuid": "lr2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d", "name": "Single Transaction Limit", "scope": "operation", "period": null, "rail_types": ["bank", "crypto"], "rail_codes": null, "operation_types": ["funding"], "asset_codes": null }, "limit_amount": "5000.00", "used_amount": "0.00", "available_amount": "5000.00", "excess_amount": "2000.00", "error_msg": "Single Transaction Limit is exceeded by 2000 EUR (the current limit is 5000 EUR)" } ] ``` The `excess_amount` of `"2000.00"` indicates the transaction exceeds the single operation limit by 2,000 EUR. #### Example: Below the Minimum Amount A minimum-amount rule has `scope: "operation_min_amount"`. The same request with an amount below the minimum returns: ```json [ { "limit_rule": { "uuid": "lr3c4d5e-6f7a-8b9c-0d1e-2f3a4b5c6d7e", "name": "Minimum transaction amount", "scope": "operation_min_amount", "period": null, "rail_types": null, "rail_codes": null, "operation_types": ["funding"], "asset_codes": null }, "limit_amount": "100.00", "used_amount": "0.00", "available_amount": "0.00", "excess_amount": "0.00", "error_msg": "Minimum transaction amount 100.00 EUR" } ] ``` For this scope `used_amount`, `available_amount` and `excess_amount` are always `"0.00"`: the rule is broken because the amount, converted to EUR, is below `limit_amount`. Read the minimum from `limit_amount`. #### Response Fields | Field | Type | Description | |-------|------|-------------| | `limit_rule` | object | The rule the operation breaks. `limit_rule.scope` is `operation` (per-transaction maximum), `period` (turnover over `limit_rule.period`) or `operation_min_amount` (per-transaction minimum) | | `limit_amount` | string | The limit threshold, in EUR, a decimal string | | `used_amount` | string | Amount already consumed (for periodic limits), a decimal string | | `available_amount` | string | Remaining capacity, a decimal string | | `excess_amount` | string | Amount by which the operation exceeds the limit, in EUR, a decimal string. Always `"0.00"` for `operation_min_amount` rules | | `error_msg` | string | Human-readable description of the breach. The same text is returned in `details` of the `oa_limit_exceeded_error` when the order is created anyway | ## Best Practices * **Check limits before creating an order.** An order that exceeds limits will be rejected. * **Display available limits to your customers** so they can adjust their transaction amounts accordingly. * **Check both per-operation and periodic limits.** A customer may have capacity for a single transaction but may have exhausted their monthly allowance. * **Treat any item in the `check-limits` response as a refusal.** The order will not be accepted at that amount; only an empty list means it passes. Do not rely on `excess_amount` being above zero alone — it stays `"0.00"` for minimum-amount rules. --- # Orders Source: https://docs.stablegate.com/docs/guide/orders An order represents a single exchange operation — converting funds from one asset to another. This page covers creating, retrieving, listing, and cancelling orders, as well as the order lifecycle. ## Order Lifecycle Every order progresses through a series of statuses: ```mermaid flowchart LR new --> processing processing <--> hold new <--> hold processing <--> failed subgraph terminal[Terminal states] cancelled(((cancelled))) rejected(((rejected))) done(((done))) stopped(((stopped))) rollbacked(((rollbacked))) end new --> cancelled new --> rejected hold --> rejected processing --> done processing --> stopped processing --> rejected failed --> stopped processing --> rollbacked failed --> rollbacked style terminal fill:none,stroke-dasharray:4 4 linkStyle 4,5,6,7,8,9,10,11,12 stroke-opacity:0.45 ``` Double circles are terminal states. The full set of transitions: | From | To | |------|----| | `new` | `processing`, `hold`, `cancelled`, `rejected` | | `processing` | `done`, `hold`, `failed`, `rejected`, `stopped`, `rollbacked` | | `hold` | `processing`, `new`, `rejected` | | `failed` | `processing` (retried by Stablegate), `stopped`, `rollbacked` | **Terminal states** — the order does not move on its own: `done`, `rejected`, `cancelled`, `stopped`, `rollbacked`. The only exception: Stablegate operations can complete a `stopped` order manually, which moves it to `done`. `failed` is **not** terminal: an order can be retried back into `processing`. `cancelled` is reachable **only from `new`**. An order that receives no deposit by `valid_until` is cancelled automatically; the check runs hourly, so the cancellation can land up to an hour after `valid_until`. ### Order Statuses | Status | Description | |--------|-------------| | `new` | Order has been created and is awaiting a deposit from the customer | | `processing` | Deposit received; the exchange is being processed | | `done` | Exchange completed successfully; funds have been delivered to the destination | | `hold` | Order has been paused for manual review | | `failed` | Order failed during processing (check `error_message` for details). **Not a final state** — Stablegate can retry it back into `processing` | | `rejected` | Order was rejected by Stablegate, or its payout was rejected by the payment provider | | `cancelled` | Order was cancelled | | `stopped` | Order was manually stopped by Stablegate operations while it was processing or after it failed; any in-flight operations were halted and the order does not advance on its own. Operations can later complete it manually (`done`) | | `rollbacked` | Order was rolled back by Stablegate operations after processing failed. Rare; final | --- ## Create an Order Submit a new exchange order for a customer. **Endpoint:** ```text POST /v1/customers/{customer_uuid}/orders/ ``` ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `source.rail` | string | Yes | Source payment rail (e.g., `sepa`, `ethereum`) | | `source.asset` | string | Yes | Source asset code (e.g., `EUR`, `USDT`) | | `source.amount` | number \| string | Yes | Amount the customer will send | | `destination.rail` | string | Yes | Destination payment rail | | `destination.asset` | string | Yes | Destination asset code | | `destination.client_account` | string | No | Destination account identifier: IBAN for fiat, wallet address for crypto | | `destination.client_account_uuid` | string (UUID) | No | UUID of a registered external account (bank account or wallet) | | `account_uuid` | string (UUID) | No | Internal account UUID (if applicable) | | `third_party_document_uuid` | string (UUID) | No | UUID of an invoice the customer already has (a document of type `proof_of_third_party_deal`, such as the one of a cancelled order) to attach to this order. See [Third-Party Payouts and the Invoice](#third-party-payouts-and-the-invoice) | > **Note:** For `destination`, provide either `client_account` (raw IBAN or wallet address) or `client_account_uuid` (UUID of a previously registered external account). Using `client_account_uuid` is recommended as it references a verified account. In both cases the destination must be an account registered for this customer; an unregistered IBAN or address is refused. > **Note:** Only `source.amount` exists. An order cannot be created from the amount the customer should receive; use [`check-fees`](/docs/guide/fees-and-limits/fees#check-fees-api) with `destination.amount` to find the source amount first. ### Example: On-Ramp Order (EUR → USDC) **Request:** ```bash curl https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/orders/ \ --request POST \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --header 'Idempotency-Key: order_c1d2e3f4_onramp_001' \ --data '{ "source": { "rail": "sepa", "asset": "EUR", "amount": 1000 }, "destination": { "rail": "ethereum", "asset": "USDC", "client_account_uuid": "wa1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d" } }' ``` **Response:** ```json { "uuid": "od1e2f3a-4b5c-6d7e-8f9a-0b1c2d3e4f5a", "reference": "SG-20250115-ABCDEF", "state": "new", "deposit_address": "", "source": { "rail": "sepa", "asset": "EUR", "amount": "1000.00", "deposit_bank_info": { "iban": "LT603250000000000001", "bank_name": "Stablegate UAB", "bank_code": "REVOLT21", "bank_country": "LT", "holder_name": "STGG AG", "bank_address": "Konstitucijos pr. 21B, 08130 Vilnius, Lithuania" }, "transaction_hash": null, "deposit_status": null }, "destination": { "rail": "ethereum", "asset": "USDC", "amount": "978.50", "client_account": "0xDC1dCb46fC96C46c4F294866AFCA81874041bB2" }, "commission": { "asset": "EUR", "amount": "15.00" }, "account": { "uuid": "ac1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f", "code": "TRD-001", "name": "Trading Account", "type": "trading" }, "error_message": null, "created_at": "2025-01-15T10:45:00.000Z", "valid_until": "2025-01-15T11:45:00.000Z" } ``` **Key response fields for on-ramp:** - `source.deposit_bank_info` — Stablegate's bank details where the customer must send the EUR transfer; the customer **must** enter `holder_name` as the recipient - `reference` — The payment reference the customer **must include** in their SEPA transfer - `valid_until` — The order will expire if no deposit is received by this time ### Example: Off-Ramp Order (USDT → EUR) **Request:** ```bash curl https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/orders/ \ --request POST \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --header 'Idempotency-Key: order_c1d2e3f4_offramp_001' \ --data '{ "source": { "rail": "ethereum", "asset": "USDT", "amount": 500 }, "destination": { "rail": "sepa", "asset": "EUR", "client_account_uuid": "ba1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e" } }' ``` **Response:** ```json { "uuid": "od2f3a4b-5c6d-7e8f-9a0b-1c2d3e4f5a6b", "reference": "SG-20250115-GHIJKL", "state": "new", "deposit_address": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18", "source": { "rail": "ethereum", "asset": "USDT", "amount": "500.000000", "deposit_bank_info": null, "transaction_hash": null, "deposit_status": null }, "destination": { "rail": "sepa", "asset": "EUR", "amount": "473.25", "client_account": "DE89370400440532013000" }, "commission": { "asset": "USDT", "amount": "10.00" }, "account": { "uuid": "ac1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f", "code": "TRD-001", "name": "Trading Account", "type": "trading" }, "error_message": null, "created_at": "2025-01-15T10:50:00.000Z", "valid_until": "2025-01-15T11:50:00.000Z" } ``` **Key response fields for off-ramp:** - `deposit_address` — The Ethereum address where the customer must send USDT - `destination.client_account` — The customer's IBAN where EUR will be delivered ### Response Fields | Field | Type | Description | |-------|------|-------------| | `uuid` | string (UUID) | Unique identifier for the order | | `reference` | string | Payment reference (must be included in fiat transfers) | | `state` | string | Current order status | | `deposit_address` | string | Crypto address for deposits (off-ramp and crypto-to-crypto) | | `source.rail` | string | Source payment rail | | `source.asset` | string | Source asset code | | `source.amount` | string | Amount to be sent by the customer | | `source.deposit_bank_info` | object \| null | Stablegate bank details for fiat deposits (on-ramp only) | | `source.deposit_bank_info.iban` | string | Stablegate IBAN the customer sends the transfer to | | `source.deposit_bank_info.bank_name` | string | Name of the Stablegate bank | | `source.deposit_bank_info.bank_code` | string | BIC of the Stablegate bank | | `source.deposit_bank_info.bank_country` | string | Country of the Stablegate bank | | `source.deposit_bank_info.holder_name` | string \| null | Payee name to put on the transfer (the Stablegate legal entity) | | `source.deposit_bank_info.bank_address` | string \| null | Address of the Stablegate bank | | `source.transaction_hash` | string \| null | Blockchain transaction hash of the customer's crypto deposit. Set once the deposit is detected; `null` before that and for bank deposits | | `source.deposit_status` | string \| null | Status of the customer's deposit: `received`, `confirmed`, `credited`, `on_hold` or `rejected` (see [Deposit Status](#deposit-status)). `null` until a deposit is detected | | `destination.rail` | string | Destination payment rail | | `destination.asset` | string | Destination asset code | | `destination.amount` | string | Amount to be received | | `destination.client_account` | string | Customer's destination account (IBAN or wallet address) | | `commission.asset` | string | Asset the commission is charged in | | `commission.amount` | string | Fee amount | | `account.uuid` | string (UUID) | Internal account UUID | | `account.type` | string | Account type (`trading` or `internal_iban`) | | `error_message` | string \| null | Error details if the order failed | | `created_at` | string (ISO 8601) | Order creation timestamp | | `valid_until` | string (ISO 8601) \| null | Deadline for the deposit. The window is set by your account configuration, so the values in the examples are illustrative. An order with no deposit by this time is cancelled; `null` once a deposit is attached | | `exchange_rate` | string \| null | Exchange rate currently applied to the order: destination asset units per one source asset unit. `null` when the order has no exchange step | | `rate_valid_until` | string (ISO 8601) \| null | Time until which `exchange_rate` and `destination.amount` are held. `null` once the deposit is received | | `third_party_document_required` | boolean | `true` when the order pays out to a third-party bank account and its source amount is above the invoice threshold. Until the invoice is attached, the order is not executed automatically. See [Third-Party Payouts and the Invoice](#third-party-payouts-and-the-invoice) | | `third_party_document_uuid` | string (UUID) \| null | The invoice attached to the order (a customer document of type `proof_of_third_party_deal`); `null` while none is attached | > **Note:** **Rate and amounts before the deposit.** While an order is `new` and has no deposit, Stablegate re-prices it periodically: `exchange_rate` and `destination.amount` are recalculated, and `rate_valid_until` shows until when the current values hold. Read the order again after that time to show the customer up-to-date numbers. When the deposit arrives, the amounts are calculated from the amount actually received, and the order is no longer re-priced. > > **Note:** the single-order endpoints return the status in a field named `state`; the list endpoint returns the same value as `status`. Read `state` when polling one order, `status` when scanning a list. The list endpoint's `status` query parameter filters on these same values. ### Deposit Status The order `state` stays `new` from the moment the customer's deposit is seen until it is credited, so `state` alone cannot tell you that the money has arrived. `source.deposit_status` fills that gap: | Value | Meaning | |-------|---------| | `null` | No deposit detected yet | | `received` | Deposit seen (on-chain or on the bank account); the compliance check is in progress | | `confirmed` | Compliance check passed; the funds are being credited to the order | | `credited` | Funds credited; the order moves to `processing` | | `on_hold` | Deposit is under manual compliance review | | `rejected` | Deposit was refused and will not fund the order; contact support for the refund | Webhooks fire only on order `state` changes, so poll [Get Order Details](#get-order-details) while the order is `new` if you want to show the customer that the deposit has been seen. ### How Deposits Are Matched **Crypto.** `deposit_address` belongs to the customer and the destination account, and is the same for every order with that destination. A transfer that lands on it is attached to the customer's newest open order on that address; the amount is not used for matching. The order is then recalculated from the amount actually received, so an over- or underpayment changes the payout rather than failing the order. Keep one open order per customer and destination at a time. A transfer smaller than 1 USDT or 1 USDC is not processed. A transfer sent over another network or in another token is not detected at all; recovery is a manual request to support. **Bank.** A SEPA transfer is matched by the `reference` from `deposit_bank_info`, with `holder_name` as the payee. A transfer without the reference stays unmatched until Stablegate support resolves it. --- ## Activate an Account with a Verification Order An account with [Standard Verification](/docs/verification/compliance/individual) trades only after activation, so `permissions.can_trade` stays `false` until the customer makes a test deposit from their own bank account. A verification order collects that deposit: it carries the bank details of the verification ledger, and Stablegate fixes the amount at one unit of the deposit asset, kept as the order commission. A regular order does not activate the account. Create the verification order instead: **Endpoint:** ```text POST /v1/customers/{customer_uuid}/orders/verification/ ``` **Request body:** | Field | Type | Required | Description | |-------|------|----------|-------------| | `source.rail` | string | Yes | Bank rail the customer pays from: `sepa`, `sepa_instant`, `swift`, `ach` or `wire` | | `source.asset` | string | Yes | Fiat asset of the deposit, for example `EUR` | | `account_uuid` | string (UUID) | No | Customer account to credit; the customer's default account when omitted | **Request:** ```bash curl https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/orders/verification/ \ --request POST \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "source": { "rail": "sepa", "asset": "EUR" } }' ``` **Response:** `200 OK` with the [order details](#get-order-details). `source.deposit_bank_info` holds the account to pay into, `reference` holds the payment reference to give to the customer, and `destination` is `null`. Show the customer the payee, the IBAN and the reference. Once the deposit arrives, the order completes on its own and `permissions.can_trade` turns `true` — poll [Get Order Details](#get-order-details) for the order and [Get Customer Details](/docs/guide/customers#get-customer-details) for the permission. > **Warning:** Only bank rails and fiat assets are accepted: a crypto rail or a crypto asset is refused with `422`. When verification is not configured for the requested rail, the call answers `400` and passes the message from Stablegate — contact your account manager to have the rail set up. --- ## Get Order Details Retrieve the current state of an order. Use this endpoint to monitor the order status. **Endpoint:** ```text GET /v1/customers/{customer_uuid}/orders/{order_uuid}/ ``` **Request:** ```bash curl https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/orders/od1e2f3a-4b5c-6d7e-8f9a-0b1c2d3e4f5a/ \ --header 'X-Api-Key: YOUR_API_KEY' ``` **Response (order in processing):** ```json { "uuid": "od1e2f3a-4b5c-6d7e-8f9a-0b1c2d3e4f5a", "reference": "SG-20250115-ABCDEF", "state": "processing", "deposit_address": "", "source": { "rail": "sepa", "asset": "EUR", "amount": "1000.00", "deposit_bank_info": { "iban": "LT603250000000000001", "bank_name": "Stablegate UAB", "bank_code": "REVOLT21", "bank_country": "LT", "holder_name": "STGG AG", "bank_address": "Konstitucijos pr. 21B, 08130 Vilnius, Lithuania" }, "transaction_hash": null, "deposit_status": "credited" }, "destination": { "rail": "ethereum", "asset": "USDC", "amount": "978.50", "client_account": "0xDC1dCb46fC96C46c4F294866AFCA81874041bB2" }, "commission": { "asset": "EUR", "amount": "15.00" }, "account": { "uuid": "ac1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f", "code": "TRD-001", "name": "Trading Account", "type": "trading" }, "error_message": null, "created_at": "2025-01-15T10:45:00.000Z", "valid_until": null } ``` ### Tracking Status Updates There are two ways to track order status changes: **Webhooks (recommended):** Configure a [webhook](/docs/guide/webhooks) to receive real-time `POST` notifications whenever an order status changes. This is more efficient and eliminates the need for polling. **Polling:** If you cannot receive webhooks, poll the order endpoint periodically: ```javascript async function waitForCompletion(customerUuid, orderUuid, intervalMs = 5000) { const terminalStatuses = ["done", "rejected", "cancelled", "stopped"]; while (true) { const order = await request("GET", `/v1/customers/${customerUuid}/orders/${orderUuid}/`); console.log(`Order ${orderUuid}: ${order.state}`); if (terminalStatuses.includes(order.state)) { return order; } await new Promise((resolve) => setTimeout(resolve, intervalMs)); } } ``` --- ## List Orders Retrieve a list of orders for a customer with optional filtering. **Endpoint:** ```text GET /v1/customers/{customer_uuid}/orders/ ``` ### Query Parameters | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `limit` | integer | 100 | Maximum number of orders to return | | `offset` | integer | 0 | Number of orders to skip (for pagination) | | `status` | array | — | Filter by status: `new`, `processing`, `done`, `hold`, `failed`, `rejected`, `cancelled`, `stopped` | | `incoming_rails_code` | string | — | Filter by source rail (e.g., `sepa`) | | `outgoing_rails_code` | string | — | Filter by destination rail (e.g., `ethereum`) | | `incoming_asset_code` | string | — | Filter by source asset (e.g., `EUR`) | | `outgoing_asset_code` | string | — | Filter by destination asset (e.g., `USDC`) | | `created_at__gte` | date | — | Orders created on or after this date (YYYY-MM-DD) | | `created_at__lte` | date | — | Orders created on or before this date | | `sort` | string | `desc` | Sort direction: `asc` or `desc` | | `order_by` | string | `id` | Field to sort by (e.g., `created_at`, `status`, `incoming_amount`) | **Request:** ```bash curl 'https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/orders/?status=done&status=processing&limit=10&sort=desc&order_by=created_at' \ --header 'X-Api-Key: YOUR_API_KEY' ``` **Response:** ```json [ { "uuid": "od1e2f3a-4b5c-6d7e-8f9a-0b1c2d3e4f5a", "reference": "SG-20250115-ABCDEF", "legal_entity_uuid": "le1a2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c", "account": { "uuid": "ac1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f", "code": "TRD-001", "name": "Trading Account", "type": "trading" }, "status": "done", "deposit_address": "", "incoming_rails": { "code": "sepa", "name": "SEPA", "type": "bank" }, "incoming_provider": null, "incoming_asset_code": "EUR", "incoming_amount": "1000.00", "outgoing_rails": { "code": "ethereum", "name": "Ethereum", "type": "crypto" }, "outgoing_provider": null, "outgoing_asset_code": "USDC", "outgoing_amount": "978.50", "outgoing_client_account": "0xDC1dCb46fC96C46c4F294866AFCA81874041bB2", "commission_amount": "15.00", "commission_asset_code": "EUR", "error_message": null, "created_at": "2025-01-15T10:45:00.000Z" } ] ``` > **Note:** The list response uses different field names than the single-order response. Notably, the list uses `status` (not `state`), `incoming_rails`/`outgoing_rails` objects, and separate `incoming_amount`/`outgoing_amount` fields. ### List Response Fields | Field | Type | Description | |-------|------|-------------| | `uuid` | string (UUID) | Order UUID | | `reference` | string | Payment reference | | `legal_entity_uuid` | string (UUID) | Legal entity UUID | | `account` | object | Internal account details | | `status` | string | Current order status (`new`, `processing`, `done`, `hold`, `failed`, `rejected`, `cancelled`, `stopped`) | | `deposit_address` | string | Crypto deposit address (empty for on-ramp) | | `incoming_rails` | object | Source rail details (`code`, `name`, `type`) | | `incoming_provider` | object \| null | Source provider details | | `incoming_asset_code` | string | Source asset code | | `incoming_amount` | string \| null | Source amount, a decimal string | | `outgoing_rails` | object | Destination rail details (`code`, `name`, `type`) | | `outgoing_provider` | object \| null | Destination provider details | | `outgoing_asset_code` | string | Destination asset code | | `outgoing_amount` | string \| null | Destination amount, a decimal string | | `outgoing_client_account` | string | Customer's destination account (IBAN or wallet address) | | `commission_amount` | string \| null | Fee amount, a decimal string | | `commission_asset_code` | string \| null | Asset the commission is charged in; the same value as `commission.asset` in the order details | | `error_message` | string \| null | Error details if the order failed | | `created_at` | string (ISO 8601) | Order creation timestamp | --- ## Cancel an Order Cancel an order that has not yet been fully processed. **Endpoint:** ```text POST /v1/customers/{customer_uuid}/orders/{order_uuid}/cancel/ ``` **Request:** ```bash curl https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/orders/od1e2f3a-4b5c-6d7e-8f9a-0b1c2d3e4f5a/cancel/ \ --request POST \ --header 'X-Api-Key: YOUR_API_KEY' ``` **Response:** `204 No Content` > **Warning:** Only orders in `new` can be cancelled. Once an order has moved to `processing`, `hold`, or any terminal state, cancelling it fails. --- ## Third-Party Payouts and the Invoice A payout to a third party's bank account, not the customer's own, needs an invoice or a contract behind it when the order is large. Stablegate decides this once, when the order is created, and returns the result in `third_party_document_required`: - Only bank payouts to a third-party account are checked. Payouts to the customer's own bank account and to crypto addresses never need an invoice. - You decide whether an account belongs to the customer or to a third party: the flag is set when the account is registered, see [External Accounts](/docs/guide/external-accounts#create-a-bank-account). The order tells the result as `third_party_document_required`. - The source amount of the order, converted to EUR, is compared with the threshold: 15 000 EUR unless your account configuration sets another value. The threshold itself does not need an invoice: an order of exactly 15 000 EUR has `third_party_document_required: false`. - When the source amount cannot be converted to EUR, the invoice is required. - The flag does not change after creation, even when the deposit differs from the order amount and the order is recalculated. An order that needs an invoice is created as usual, without an error. Until the invoice is attached it is not executed automatically: after the deposit is confirmed, the order stays `new` with `source.deposit_status: confirmed` and waits for Stablegate operations, who can approve and execute it manually. Once the invoice is attached, the order is processed under the usual auto-processing rules; if the deposit has already arrived, processing resumes. There are two ways to give the invoice: - **After creation (recommended):** upload the file with [Attach an Invoice](#attach-an-invoice). The order must still be `new`. - **At creation:** pass `third_party_document_uuid` with the UUID of an invoice the customer already has, for example the `third_party_document_uuid` of a cancelled order that you create again. A document can back one active order only; the document of a cancelled or rejected order is free to use again. Stablegate operations review the content of the document; the API does not check it. ### Attach an Invoice Uploads the invoice or contract file and attaches it to the order in one call. **Endpoint:** ```text POST /v1/customers/{customer_uuid}/orders/{order_uuid}/third-party-document/ ``` The request body is `multipart/form-data`: | Field | Type | Required | Description | |-------|------|----------|-------------| | `file` | file | Yes | Invoice or contract file | | `name` | string | No | Document name; the file name by default | **Request:** ```bash curl 'https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/orders/od3a4b5c-6d7e-8f9a-0b1c-2d3e4f5a6b7c/third-party-document/' \ --request POST \ --header 'X-Api-Key: YOUR_API_KEY' \ --form 'file=@invoice-2025-0142.pdf' \ --form 'name=Invoice 2025-0142' ``` **Response:** `200 OK` with the [order details](#get-order-details); `third_party_document_uuid` now holds the attached document: ```json { "uuid": "od3a4b5c-6d7e-8f9a-0b1c-2d3e4f5a6b7c", "reference": "SG-20250115-MNOPQR", "state": "new", "deposit_address": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18", "source": { "rail": "ethereum", "asset": "USDT", "amount": "20000.000000", "deposit_bank_info": null, "transaction_hash": null, "deposit_status": null }, "destination": { "rail": "sepa", "asset": "EUR", "amount": "19320.00", "client_account": "FR7630006000011234567890189" }, "commission": { "asset": "EUR", "amount": "10.00" }, "account": { "uuid": "ac1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f", "code": "TRD-001", "name": "Trading Account", "type": "trading" }, "error_message": null, "third_party_document_required": true, "third_party_document_uuid": "dc4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d", "created_at": "2025-01-15T11:00:00.000Z", "valid_until": "2025-01-15T12:00:00.000Z" } ``` The call is refused with `400` when the order does not need an invoice, already has one, or is no longer `new`; the file is not stored in these cases. An order that is not found answers `404`. > **Warning:** **Retries.** A re-sent multipart body differs from the first one, so a retry with the same `Idempotency-Key` is answered with `422` instead of the cached response. After a timeout or a `5xx`, read the order first: when `third_party_document_uuid` is set, the invoice is attached and the call must not be repeated. ## Best Practices - **Always check fees and limits** before creating an order to avoid failures. - **Use `client_account_uuid`** instead of raw account identifiers when possible — it references a registered and verified account. - **Store the order `uuid`** on your side immediately after creation for tracking and reconciliation. - **Use [webhooks](/docs/guide/webhooks)** to receive real-time order status updates instead of polling. - **If polling**, use a reasonable interval (e.g., every 5–10 seconds) and stop when a terminal status is reached. - **Handle all terminal states**: `done` (success), `rejected`, `cancelled`, `stopped` — each may require different handling in your application. `failed` is **not** terminal: an order can move from `failed` back to `processing`, so don't treat it as the end of the line. - **Instruct your customers** to include the `reference` in SEPA transfers, otherwise the deposit may not be matched to the order. - **Check `third_party_document_required`** in the create response and [attach the invoice](#attach-an-invoice) right away when it is `true`; without it the order waits for Stablegate operations. --- # Webhooks Source: https://docs.stablegate.com/docs/guide/webhooks Webhooks allow your application to receive real-time notifications from Stablegate when events occur — such as an order status change. Instead of polling the API, you register an HTTPS endpoint and Stablegate sends a signed `POST` request to it automatically. ## How It Works 1. You register a webhook endpoint in the Developers (https://app.stablegate.com/developers) section. 2. When an event occurs (e.g., an order moves from `new` to `processing`), Stablegate sends a `POST` request to your endpoint. 3. Your server verifies the signature, processes the event, and responds with any `2xx` status code. --- ## Events A webhook event has two identifiers, and they are not the same string: - The **event type key** — `dev__order_status_changed` — is what a webhook is registered against in the **Developers** section. - The **event name in the payload** — `order.status_changed` — is what the `event` field of the delivered JSON contains. | Event type key | Payload `event` | Description | |----------------|-----------------|-------------| | `dev__order_status_changed` | `order.status_changed` | Fired when the status of an order changes. Includes the previous and current status, order details, and amounts. | This guide documents that event. The form may offer more event types depending on your account. ### When the event fires You receive the event for orders **your integration created through the API**. The notification is addressed to the account that created the order, so an order created by any other route — for example by Stablegate staff on your behalf — does not reach your webhook. Delivery is dispatched roughly 10 seconds after the status change is committed. > **Note:** Additional event types may be added in the future. Your webhook handler should gracefully ignore unrecognized event types. --- ## Webhook Payload Every webhook request is a `POST` with a JSON body. Here is the payload for `order.status_changed`: ```json { "event": "order.status_changed", "order_uuid": "0d1e2f3a-4b5c-6d7e-8f9a-0b1c2d3e4f5a", "external_id": "SG-20250115-ABCDEF", "status": "processing", "previous_status": "new", "customer_uuid": "c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f", "incoming": { "amount": "1000.00", "asset": "EUR", "rail": "sepa" }, "outgoing": { "amount": "978.50", "asset": "USDC", "rail": "ethereum" }, "timestamp": "2025-01-15 10:30:00+00:00" } ``` ### Payload Fields | Field | Type | Description | |-------|------|-------------| | `event` | string | Event name (currently always `order.status_changed`) | | `order_uuid` | string (UUID) | Order identifier | | `external_id` | string | Order reference (same as `reference` in the Orders API) | | `status` | string | New order status | | `previous_status` | string | Previous order status | | `customer_uuid` | string (UUID) | Customer identifier | | `incoming.amount` | string | Source amount | | `incoming.asset` | string | Source asset code (e.g., `EUR`) | | `incoming.rail` | string | Source rail (e.g., `sepa`) | | `outgoing.amount` | string | Destination amount | | `outgoing.asset` | string | Destination asset code (e.g., `USDC`) | | `outgoing.rail` | string | Destination rail (e.g., `ethereum`) | | `timestamp` | string | Time the **order was created** — not the time of the status change | > **Note:** **`timestamp` is the order's creation time.** Despite the field name, it does not change > between deliveries for the same order and is not the time of the status transition, so do not > use it to order events. Use your own receipt time, and treat the `previous_status` → `status` > pair as the transition identity. ### Order Statuses The `status` and `previous_status` fields use the same values as the [Orders API](/docs/guide/orders#order-statuses): `new`, `processing`, `done`, `hold`, `failed`, `rejected`, `cancelled`, `stopped`, `rollbacked`. --- ## Verifying Webhook Signatures Every webhook request includes an `X-Api-Sig` header containing an HMAC-SHA512 signature. You **must** verify this signature to confirm the request originated from Stablegate. The signature is computed over the raw request body using the secret you provided when creating the webhook. ### Verification Steps 1. Read the raw HTTP request body as bytes — do not parse or re-serialize it. 2. Compute HMAC-SHA512 using your webhook secret and the raw body. 3. Compare the result with the `X-Api-Sig` header value. ### Python ```python import hmac import hashlib def verify_webhook(body: bytes, signature: str, secret: str) -> bool: expected = hmac.new( secret.encode(), body, digestmod=hashlib.sha512, ).hexdigest() return hmac.compare_digest(expected, signature) # In your webhook handler # body = request.body (raw bytes, not parsed JSON) # signature = request.headers["X-Api-Sig"] # secret = "your-webhook-secret" ``` ### JavaScript (Node.js) ```javascript const crypto = require("crypto"); function verifyWebhook(body, signature, secret) { const expected = crypto .createHmac("sha512", secret) .update(body) .digest("hex"); return crypto.timingSafeEqual( Buffer.from(expected), Buffer.from(signature), ); } // In your Express handler: // app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => { // const isValid = verifyWebhook(req.body, req.headers["x-api-sig"], SECRET); // if (!isValid) return res.status(401).send("Invalid signature"); // const event = JSON.parse(req.body); // // process event... // res.status(200).send("OK"); // }); ``` > **Warning:** Always use the raw request body for signature verification. Parsing the JSON and re-serializing it may change whitespace or field ordering, causing the signature check to fail. --- ## Managing Webhooks Webhooks are managed in the Developers (https://app.stablegate.com/developers) section, on the **Webhooks** tab. ### Creating a Webhook 1. Open the **Webhooks** tab in the **Developers** section. 2. Click **Add webhook**. 3. Fill in **Name** (up to 50 characters). 4. Fill in **Endpoint URL** (up to 500 characters, `https://` only). 5. Set a **Signing secret** (up to 50 characters), used for HMAC-SHA512 signature verification. **Generate** fills it with 32 random hex characters. 6. Tick the events the webhook should receive. At least one is required; the list depends on your account type. ### Requirements | Field | Constraint | Description | |-------|------------|-------------| | Name | 1–50 characters | Label for the webhook in the Developers section | | Endpoint URL | Up to 500 characters; the form requires `https://` | Your webhook endpoint URL | | Signing secret | Up to 50 characters | Used to sign webhook payloads | > **Note:** **Use HTTPS.** Always register an `https://` endpoint. The form enforces this; the API itself > does not check the scheme, and webhook payloads carry order and customer identifiers whose > signature is only meaningful over an encrypted channel. > > **Choose a strong secret.** No minimum length is enforced, so a weak secret will be accepted. > Use at least 32 random characters — the secret is the only thing preventing a third party from > forging a valid signature. Webhooks are managed only in the Developers section. Webhook management is not exposed on the Partner API. --- ## Delivery and Retries - Stablegate considers a webhook **delivered** when your server responds with any `2xx` HTTP status code. - If your server returns a non-`2xx` status or does not respond within **60 seconds**, Stablegate will **retry** the delivery. - **Redirects are not followed.** A `3xx` response counts as a failed delivery, so register the final URL rather than one that redirects (for example, include the trailing slash if your server adds one). - Order status webhooks are retried up to **3 times**, roughly two minutes apart. - The attempt count is configured per event type on the Stablegate side and may change. - Once the attempts are exhausted, the notification is marked as failed and is not retried again. There is no endpoint to replay missed deliveries: after an outage on your side, reconcile by fetching the orders you track with `GET /v1/customers/{customer_uuid}/orders/{order_uuid}/`. ### Best Practices - **Respond quickly.** Return a `200 OK` as soon as you receive the webhook. Process the event asynchronously if it involves heavy operations. - **Be idempotent.** The same event may be delivered more than once (e.g., due to retries). Use the order identifier and the new status together to detect duplicates, and make reprocessing a no-op. - **Use HTTPS.** Register an `https://` endpoint. Plain `http://` targets are not currently rejected by validation, but they leave payloads readable in transit. - **Verify signatures.** Always verify the `X-Api-Sig` header before processing any webhook. This prevents spoofed requests. - **Return `2xx` for known events, even if you choose not to act on them.** Returning an error code will trigger retries. - **Log webhook payloads.** Store raw payloads for debugging and audit purposes. --- ## Example: Handling Webhooks A minimal webhook handler that verifies the signature and processes order status changes: ### Python (Flask) ```python import hmac import hashlib import json from flask import Flask, request, abort app = Flask(__name__) WEBHOOK_SECRET = "your-webhook-secret-at-least-32-chars" @app.post("/webhook") def handle_webhook(): body = request.get_data() signature = request.headers.get("X-Api-Sig", "") expected = hmac.new( WEBHOOK_SECRET.encode(), body, digestmod=hashlib.sha512 ).hexdigest() if not hmac.compare_digest(expected, signature): abort(401) event = json.loads(body) if event["event"] == "order.status_changed": print(f"Order {event['order_uuid']}: {event['previous_status']} → {event['status']}") # Update your internal records, notify your customer, etc. return "OK", 200 ``` ### JavaScript (Express) ```javascript const express = require("express"); const crypto = require("crypto"); const app = express(); const WEBHOOK_SECRET = "your-webhook-secret-at-least-32-chars"; app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => { const signature = req.headers["x-api-sig"] || ""; const expected = crypto .createHmac("sha512", WEBHOOK_SECRET) .update(req.body) .digest("hex"); if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) { return res.status(401).send("Invalid signature"); } const event = JSON.parse(req.body); if (event.event === "order.status_changed") { console.log(`Order ${event.order_uuid}: ${event.previous_status} → ${event.status}`); // Update your internal records, notify your customer, etc. } res.status(200).send("OK"); }); app.listen(3000); ``` --- # Integration Guides Source: https://docs.stablegate.com/docs/guide/integration-guides This section provides a checklist for the whole integration and end-to-end walkthroughs for the two main exchange flows: - **Integration Checklist** — The path from API access to go-live in four phases, with the mistakes that cost integrators the most time. - **On-Ramp (Fiat to Crypto)** — A step-by-step guide to converting EUR to USDC, covering wallet registration, pre-order checks, order creation, customer deposit instructions, and status monitoring. - **Off-Ramp (Crypto to Fiat)** — A step-by-step guide to converting USDT to EUR, covering bank account registration, pre-order checks, order creation, customer deposit instructions, and status monitoring. Each guide follows a complete scenario from start to finish, with request/response examples at every step. --- # Integration Checklist Source: https://docs.stablegate.com/docs/guide/integration-guides/checklist The path from the first request to go-live, in four phases. Each item is a question to answer before moving on, with a pointer to the page that has the details. [Coverage](/docs/guide/getting-started/coverage) says what the API supports; this page says in what order to use it. ## What You Are Building Your product lets your users exchange stablecoins for euros, or euros for stablecoins, without leaving your application. Stablegate provides the regulated entity, customer verification and screening, bank and blockchain settlement, and the order lifecycle. Your side owns the user: sign-up, the verification hand-off, the screen with deposit instructions, and the status the user sees afterwards. Stablegate does not email deposit instructions for orders you create through the API, so your screen is the only place the user sees them. Stablegate still emails the user when the deposit arrives and when the payout is sent. From the user's point of view: sign up in your app, pass verification once, add the account the money should go to, see a quote, send the deposit, receive the payout. ## Phase 1: Prepare - [ ] **Which flows do you need?** On-ramp, off-ramp or both. Confirm the pairs with [`GET /v1/exchange-routes/`](/docs/guide/exchange-routes); the API exchanges only what that endpoint returns. - [ ] **Who are your customers?** Individuals can use [Instant Verification](/docs/guide/customers#start-instant-verification) and trade within minutes; corporate customers go through the hosted Standard flow and activation. Ask your account manager to enable Instant Verification if you plan to use it. - [ ] **Where does the payout go?** To a bank account or wallet registered for the customer. If your product needs anything else, agree it with Stablegate before you build. - [ ] **Do you have access?** Partner API access shows **API keys**, **Webhooks** and **Your clients** in the Developers section; without it every call answers `403`. See [Getting Started](/docs/guide/getting-started). - [ ] **Does the smoke test pass?** `GET /v1/exchange-routes/` from your backend answers `200` with `X-Api-Key` and a trailing slash on the path. A missing slash answers `307`, and most HTTP clients drop the header on redirect. See [Authentication](/docs/guide/getting-started/authentication). - [ ] **Is your client generated from the [OpenAPI specification](/openapi.json)?** Required fields are validated by the API; optional fields are not, so a typo in an optional field name silently falls back to the default. ## Phase 2: Build **Customers and verification** ([Customers](/docs/guide/customers)) - [ ] **Do you store the customer `uuid` and your own `external_reference`?** Create the customer when the user enters the exchange feature and keep the mapping; a customer cannot be edited later. - [ ] **Do you start verification explicitly?** It never starts on its own. Instant Verification needs `first_name`, `last_name` and `date_of_birth`; the Standard link is valid for **30 minutes**. - [ ] **Do you gate trading on `state: active` and `permissions.can_trade: true`?** Poll `GET /v1/customers/{customer_uuid}/` after submission. `kyc_level` alone is not enough for a Standard customer, who also needs activation. **Accounts** ([External Accounts](/docs/guide/external-accounts)) - [ ] **Is the payout account registered before the first order?** A bank account for off-ramp, a wallet for on-ramp. Keep the `uuid` to pass as `destination.client_account_uuid`. - [ ] **Do you collect the wallet ownership proof at registration time?** Ethereum wallets need the signed message, with a timestamp less than **24 hours** old. A duplicate address answers `400` `oa_client_external_account_address_exists`; ask the user for another wallet. **Orders** ([Fees](/docs/guide/fees-and-limits/fees), [Limits](/docs/guide/fees-and-limits/limits), [Orders](/docs/guide/orders)) - [ ] **Do you run the three checks before each order?** `POST /v1/exchange-routes/check/` answers `available: true`; `check-fees` gives the quote you show as an estimate; `check-limits` answers an empty list. Any item in that list means refusal. - [ ] **Do you send an `Idempotency-Key`?** Generate it before the first attempt and reuse the same key only when retrying the same request. Retry network errors and `5xx`; do not retry `4xx` except `409`. See [Idempotency](/docs/guide/getting-started/idempotency). - [ ] **Is the deposit screen rendered from the order you just created?** On-ramp: `deposit_bank_info` with `holder_name` as the payee and the `reference` the user must copy exactly. Off-ramp: `deposit_address`, the network and `source.amount`. Both show `valid_until` as the deadline and `destination.amount` as an estimate until the deposit lands. - [ ] **Is there one open order per customer and destination?** Crypto deposits are matched by address and attach to the newest open order. See [How Deposits Are Matched](/docs/guide/orders#how-deposits-are-matched). **Monitoring** ([Webhooks](/docs/guide/webhooks)) - [ ] **Does your webhook endpoint handle deliveries correctly?** Verify `X-Api-Sig` over the raw body before parsing, answer `2xx` within **60 seconds**, and ignore event types you do not know. - [ ] **Do you deduplicate and order events yourself?** Deduplicate on `order_uuid` plus `status` and order by your receipt time; the payload `timestamp` is the order creation time. - [ ] **Do you poll open orders as a fallback?** A delivery that fails its retries is not replayed. While `state` is `new`, the same poll reads `source.deposit_status`. - [ ] **Is `failed` non-terminal in your status model?** Final states are `done`, `rejected`, `cancelled`, `stopped` and `rollbacked`. ## Phase 3: Test - [ ] **Has one on-ramp and one off-ramp order reached `done`?** End to end, with a real deposit. - [ ] **Have the negative paths run?** A route that is unavailable, a limit that is exceeded, an order left to expire past `valid_until`, a cancelled order, a webhook endpoint that was down during a status change and caught up by polling. - [ ] **Do your logs keep `trace_id` from every error response?** Support asks for it first. ## Phase 4: Go Live - [ ] **Are support contacts exchanged in both directions?** The cases that need them: a deposit without reference, a transfer on the wrong network, a rejected deposit, a customer that needs closing or unblocking. - [ ] **Is key rotation rehearsed?** Create a new key, switch, delete the old one; deletion takes effect immediately. - [ ] **Do you follow changes?** The [Changelog](/docs/changelog) lists every change, and Stablegate sends each partner a personal digest of what affects their integration. ## Common Mistakes | Mistake | What happens | What to do instead | |---------|--------------|--------------------| | Path without the trailing slash | `307`, then `401` because the client dropped `X-Api-Key` on redirect | End every path with `/` | | Treating `check-fees` as a locked quote | The payout differs from what the user saw | Show the quote as an estimate; read `exchange_rate` and `rate_valid_until` from the order | | Reading `excess_amount > 0` as "limit exceeded" | A breached minimum-amount rule is missed | Any item in the `check-limits` response means refusal | | Waiting for a webhook for verification or deposit | Nothing arrives | Poll the customer detail for `can_trade`, and the order detail for `deposit_status` | | Creating a second order while one is open on the same address | The deposit attaches to the newest order | One open order per customer and destination | | Re-serializing the webhook body before verifying the signature | Every signature check fails | Verify over the raw bytes | | Ordering events by the webhook `timestamp` | Transitions appear out of order | Use your receipt time and the `previous_status` to `status` pair | | Treating `failed` as final | The order later moves to `processing` or `done` and your user sees a wrong status | Keep polling until a final state | | Treating `404` on a customer UUID as an auth problem | Time lost on key checks | `404` means not your customer; `403` means the key lacks access | | Parsing amounts as floats | Rounding errors and refused orders | Use a decimal type and `fraction_digits` | --- # Guide: On-Ramp (Fiat to Crypto) Source: https://docs.stablegate.com/docs/guide/integration-guides/onramp This guide walks you through a complete on-ramp flow — converting EUR to USDC via the Stablegate API. By the end, your customer will have USDC delivered to their Ethereum wallet. **Scenario:** Your customer wants to buy 1,000 EUR worth of USDC, receiving the tokens at their Ethereum wallet address. ## Prerequisites Before you begin, make sure you have: - [ ] API key configured - [ ] Customer UUID (created via the [Customers API](/docs/guide/customers)) ## Flow Overview 1. **Register wallet** — register the customer's Ethereum wallet. 2. **Check route** — verify SEPA/EUR → Ethereum/USDC is available. 3. **Check fees** — calculate the fee for 1,000 EUR. 4. **Check limits** — confirm the amount is within limits. 5. **Create order** — submit the on-ramp order. 6. **Customer deposit** — the customer sends EUR via SEPA transfer. 7. **Monitor status** — poll until the order reaches a terminal state. 8. **Complete** — USDC is delivered to the customer's wallet. --- ## Step 1: Register the Customer's Ethereum Wallet Before creating an on-ramp order, register the wallet that will receive the crypto. For Ethereum wallets, ownership verification is required. **Construct the ownership proof:** ```javascript import { ethers } from "ethers"; function stringToHex(str) { let hex = ""; for (let i = 0; i < str.length; i++) { hex += str.charCodeAt(i).toString(16); } return `0x${hex}`; } // Customer's wallet const wallet = new ethers.Wallet("CUSTOMER_PRIVATE_KEY"); const ts = new Date().toISOString(); const message = `I confirm the ownership of address ${wallet.address}. Signature time: ${ts}.`; const wallet_signature = await wallet.signMessage(message); const wallet_message = stringToHex(message); ``` **Request:** ```bash curl https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/wallets/ \ --request POST \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "rail": "ethereum", "address": "0xDC1dCb46fC96C46c4F294866AFCA81874041bB2", "name": "Customer Main Wallet", "wallet_message": "0x4920636f6e6669726d20746865206f776e657273686970206f662061646472657373203078444331644362343666433936433436633446323934383636364146434138313837343034316242322e205369676e61747572652074696d653a20323032352d30312d31355431303a33303a30302e3030305a2e", "wallet_signature": "0x861818884bbf61f8d83bc3052e6c513f939512852fc92067dde5845b392248c949812e44366158d91d73ac0e9588c56dfb1d59549c3cbad52cfe4884fdc15c361" }' ``` **Response:** ```json { "uuid": "wa1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d", "name": "Customer Main Wallet", "rail": "ethereum", "address": "0xDC1dCb46fC96C46c4F294866AFCA81874041bB2", "created_at": "2025-01-15T10:30:00.000Z", "updated_at": "2025-01-15T10:30:00.000Z" } ``` Save the `uuid` — you will use it when creating the order. > **Note:** If the wallet is already registered, skip this step and use the existing wallet UUID. --- ## Step 2: Check Exchange Route Availability Verify that the SEPA EUR → Ethereum USDC route is currently available. **Request:** ```bash curl https://api.stablegate.com/v1/exchange-routes/check/ \ --request POST \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "source": { "rail": "sepa", "asset_code": "EUR" }, "destination": { "rail": "ethereum", "asset_code": "USDC" } }' ``` **Response:** ```json { "available": true, "route": { "source": { "rail": "sepa", "asset": { "code": "EUR", "fraction_digits": 2 } }, "destination": { "rail": "ethereum", "asset": { "code": "USDC", "fraction_digits": 6 } } } } ``` The route is available. If `available` were `false`, you would need to inform the customer that this conversion path is temporarily unavailable. --- ## Step 3: Calculate Fees Check how much the customer will receive for 1,000 EUR after fees. **Request:** ```bash curl https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/check-fees/ \ --request POST \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "source": { "rail": "sepa", "asset": "EUR", "amount": 1000 }, "destination": { "rail": "ethereum", "asset": "USDC" } }' ``` **Response:** ```json { "source": { "rail": "sepa", "asset": "EUR", "amount": "1000.00" }, "destination": { "rail": "ethereum", "asset": "USDC", "amount": "978.50" }, "commission": { "asset": "EUR", "amount": "15.00" }, "exchange_rate": "1.006700" } ``` **Display to the customer:** - Sends: **1,000.00 EUR** - Fee: **15.00 EUR** - Exchange rate: **1.0067** (EUR/USDC) - Receives: **~978.50 USDC** > **Note:** The final amount may vary slightly due to exchange rate fluctuations between the fee check and order execution. --- ## Step 4: Verify Limits Confirm the customer has sufficient limit capacity for this transaction. **Request:** ```bash curl https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/check-limits/ \ --request POST \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "source": { "rail": "sepa", "asset": "EUR", "amount": 1000 }, "destination": { "rail": "ethereum", "asset": "USDC" } }' ``` **Response — the operation passes:** ```json [] ``` The endpoint returns only the rules the operation breaks, so an empty array is the success case. **Response — a limit is exceeded:** ```json [ { "limit_rule": { "uuid": "lr1a2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c", "name": "Lifetime turnover", "scope": "period", "period": "lifetime", "rail_types": ["bank"], "rail_codes": null, "operation_types": ["funding", "payout"], "asset_codes": null }, "limit_amount": "15000.00", "used_amount": "14500.00", "available_amount": "500.00", "excess_amount": "500.00", "error_msg": "Lifetime turnover is exceeded by 500 EUR (the current limit is 15000 EUR)" } ] ``` > **Note:** **Any element in the array is a refusal**, even when `excess_amount` is `"0.00"`: the minimum-amount rule reports itself that way, and the reason sits in `error_msg`. Show the customer `available_amount` and suggest a smaller transaction. --- ## Step 5: Create the On-Ramp Order Submit the order using the wallet UUID from Step 1. **Request:** ```bash curl https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/orders/ \ --request POST \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --header 'Idempotency-Key: order_c1d2e3f4_onramp_001' \ --data '{ "source": { "rail": "sepa", "asset": "EUR", "amount": 1000 }, "destination": { "rail": "ethereum", "asset": "USDC", "client_account_uuid": "wa1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d" } }' ``` **Response:** ```json { "uuid": "od1e2f3a-4b5c-6d7e-8f9a-0b1c2d3e4f5a", "reference": "SG-20250115-ABCDEF", "state": "new", "deposit_address": "", "source": { "rail": "sepa", "asset": "EUR", "amount": "1000.00", "deposit_bank_info": { "iban": "LT603250000000000001", "bank_name": "Stablegate UAB", "bank_code": "REVOLT21", "bank_country": "LT", "holder_name": "STGG AG", "bank_address": "Konstitucijos pr. 21B, 08130 Vilnius, Lithuania" }, "transaction_hash": null, "deposit_status": null }, "destination": { "rail": "ethereum", "asset": "USDC", "amount": "978.50", "client_account": "0xDC1dCb46fC96C46c4F294866AFCA81874041bB2" }, "commission": { "asset": "EUR", "amount": "15.00" }, "account": { "uuid": "ac1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f", "code": "TRD-001", "name": "Trading Account", "type": "trading" }, "error_message": null, "created_at": "2025-01-15T10:45:00.000Z", "valid_until": "2025-01-15T11:45:00.000Z" } ``` Save the order `uuid` for tracking. Take note of these critical fields: | Field | Value | Purpose | |-------|-------|---------| | `source.deposit_bank_info.iban` | `LT603250000000000001` | Stablegate's bank account where the customer sends EUR | | `source.deposit_bank_info.holder_name` | `STGG AG` | Payee name the customer enters as the recipient of the transfer | | `reference` | `SG-20250115-ABCDEF` | Must be included as the payment reference in the SEPA transfer | | `valid_until` | `2025-01-15T11:45:00.000Z` | Deadline for the deposit | --- ## Step 6: Customer Sends EUR via SEPA Instruct your customer to initiate a SEPA bank transfer with the following details: | Detail | Value | |--------|-------| | **Recipient IBAN** | `LT603250000000000001` | | **Recipient Name** | `STGG AG` — the `holder_name` from the order response | | **Bank Name** | `Stablegate UAB` — the `bank_name` field, the bank that holds the account | | **Amount** | `1,000.00 EUR` | | **Payment Reference** | `SG-20250115-ABCDEF` | > **Warning:** The payment reference must exactly match the `reference` from the order response. Without it, the deposit cannot be automatically matched to the order. --- ## Step 7: Monitor Order Status After the customer initiates the SEPA transfer, poll the order status. SEPA transfers typically settle within 1 business day. **Request:** ```bash curl https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/orders/od1e2f3a-4b5c-6d7e-8f9a-0b1c2d3e4f5a/ \ --header 'X-Api-Key: YOUR_API_KEY' ``` **Status transitions you will observe:** 1. **`new`** — Order created, waiting for deposit. `source.deposit_status` stays `null` until the transfer arrives 2. **`new`** with `source.deposit_status: "received"` or `"confirmed"` — the transfer is on Stablegate's account and is being matched. `"on_hold"` means compliance holds it, `"rejected"` means it will not be credited 3. **`processing`** — EUR deposit credited, exchange in progress 4. **`done`** — Exchange complete, USDC sent to the customer's wallet **Response (completed):** ```json { "uuid": "od1e2f3a-4b5c-6d7e-8f9a-0b1c2d3e4f5a", "reference": "SG-20250115-ABCDEF", "state": "done", "deposit_address": "", "source": { "rail": "sepa", "asset": "EUR", "amount": "1000.00", "deposit_bank_info": { "iban": "LT603250000000000001", "bank_name": "Stablegate UAB", "bank_code": "REVOLT21", "bank_country": "LT", "holder_name": "STGG AG", "bank_address": "Konstitucijos pr. 21B, 08130 Vilnius, Lithuania" }, "transaction_hash": null, "deposit_status": "credited" }, "destination": { "rail": "ethereum", "asset": "USDC", "amount": "978.50", "client_account": "0xDC1dCb46fC96C46c4F294866AFCA81874041bB2" }, "commission": { "asset": "EUR", "amount": "15.00" }, "account": { "uuid": "ac1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f", "code": "TRD-001", "name": "Trading Account", "type": "trading" }, "error_message": null, "created_at": "2025-01-15T10:45:00.000Z", "valid_until": null, "exchange_rate": "0.9785", "rate_valid_until": null } ``` --- ## Step 8: Order Complete The order is in the `done` state. The customer's Ethereum wallet (`0xDC1dCb46...`) has received **978.50 USDC**. **Summary:** | Item | Value | |---|---| | **Customer sent** | 1,000.00 EUR via SEPA | | **Fee** | 15.00 EUR | | **Customer received** | 978.50 USDC at `0xDC1d...41bB2` | | **Status** | `done` | --- ## Error Handling ### Route unavailable If the route check in Step 2 returns `available: false`, the conversion path is temporarily unavailable. Inform the customer and suggest trying again later. ### Limits exceeded If the check in Step 4 returns a non-empty array, the transaction breaks a limit. Do not look at `excess_amount` alone: the minimum-amount rule reports itself with `excess_amount: "0.00"` and the reason in `error_msg`. Show `available_amount` and suggest a smaller transaction. ### Order failed If the order reaches a `failed` or `rejected` state, check the `error_message` field for details. A transfer that arrives with a different amount does not fail the order by itself: the payout is recalculated from the amount actually received, so an under- or overpayment changes the amount the customer gets. A transfer that arrives without the `reference` cannot be matched automatically and is handled by Stablegate support. ### Order expired If the customer does not send the SEPA transfer before `valid_until`, the order may expire. Create a new order if needed. ### Order cancellation To cancel an order before the deposit is received: ```bash curl https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/orders/od1e2f3a-4b5c-6d7e-8f9a-0b1c2d3e4f5a/cancel/ \ --request POST \ --header 'X-Api-Key: YOUR_API_KEY' ``` **Response:** `204 No Content` --- # Guide: Off-Ramp (Crypto to Fiat) Source: https://docs.stablegate.com/docs/guide/integration-guides/offramp This guide walks you through a complete off-ramp flow — converting USDT to EUR via the Stablegate API. By the end, your customer will have EUR delivered to their bank account. **Scenario:** Your customer wants to sell 500 USDT, receiving EUR at their SEPA bank account. ## Prerequisites Before you begin, make sure you have: - [ ] API key configured - [ ] Customer UUID (created via the [Customers API](/docs/guide/customers)) ## Flow Overview 1. **Register bank account** — register the customer's IBAN. 2. **Check route** — verify Ethereum/USDT → SEPA/EUR is available. 3. **Check fees** — calculate the fee for 500 USDT. 4. **Check limits** — confirm the amount is within limits. 5. **Create order** — submit the off-ramp order; attach the invoice if the order needs one. 6. **Customer deposit** — the customer sends USDT to the deposit address. 7. **Monitor status** — poll until the order reaches a terminal state. 8. **Complete** — EUR is delivered to the customer's bank account. --- ## Step 1: Register the Customer's Bank Account Register the IBAN where the customer will receive EUR. **Request:** ```bash curl https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/bank-accounts/ \ --request POST \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "iban": "DE89370400440532013000", "holder_name": "Max Mustermann", "name": "Main EUR Account", "bank_name": "Commerzbank", "bank_code": "COBADEFFXXX" }' ``` **Response:** ```json { "uuid": "ba1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e", "name": "Main EUR Account", "iban": "DE89370400440532013000", "bank_name": "Commerzbank", "bank_code": "COBADEFFXXX", "bank_country": "DE", "bank_address": null, "holder_name": "Max Mustermann", "created_at": "2025-01-15T10:30:00.000Z", "updated_at": "2025-01-15T10:30:00.000Z" } ``` Save the `uuid` — you will use it when creating the order. > **Note:** If the bank account is already registered, skip this step and use the existing bank account UUID. --- ## Step 2: Check Exchange Route Availability Verify that the Ethereum USDT → SEPA EUR route is currently available. **Request:** ```bash curl https://api.stablegate.com/v1/exchange-routes/check/ \ --request POST \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "source": { "rail": "ethereum", "asset_code": "USDT" }, "destination": { "rail": "sepa", "asset_code": "EUR" } }' ``` **Response:** ```json { "available": true, "route": { "source": { "rail": "ethereum", "asset": { "code": "USDT", "fraction_digits": 6 } }, "destination": { "rail": "sepa", "asset": { "code": "EUR", "fraction_digits": 2 } } } } ``` The route is available. --- ## Step 3: Calculate Fees Check how much EUR the customer will receive for 500 USDT after fees. **Request:** ```bash curl https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/check-fees/ \ --request POST \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "source": { "rail": "ethereum", "asset": "USDT", "amount": 500 }, "destination": { "rail": "sepa", "asset": "EUR" } }' ``` **Response:** ```json { "source": { "rail": "ethereum", "asset": "USDT", "amount": "500.000000" }, "destination": { "rail": "sepa", "asset": "EUR", "amount": "473.25" }, "commission": { "asset": "USDT", "amount": "10.00" }, "exchange_rate": "0.966500" } ``` **Display to the customer:** - Sends: **500.00 USDT** - Fee: **10.00 EUR** - Exchange rate: **0.9665** (USDT/EUR) - Receives: **~473.25 EUR** to their bank account --- ## Step 4: Verify Limits Confirm the customer has sufficient limit capacity. **Request:** ```bash curl https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/check-limits/ \ --request POST \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "source": { "rail": "ethereum", "asset": "USDT", "amount": 500 }, "destination": { "rail": "sepa", "asset": "EUR" } }' ``` **Response — the operation passes:** ```json [] ``` The endpoint returns only the rules the operation breaks, so an empty array is the success case. **Response — a limit is exceeded:** ```json [ { "limit_rule": { "uuid": "lr1a2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c", "name": "Lifetime turnover", "scope": "period", "period": "lifetime", "rail_types": ["bank"], "rail_codes": null, "operation_types": ["funding", "payout"], "asset_codes": null }, "limit_amount": "15000.00", "used_amount": "14500.00", "available_amount": "500.00", "excess_amount": "500.00", "error_msg": "Lifetime turnover is exceeded by 500 EUR (the current limit is 15000 EUR)" } ] ``` > **Note:** **Any element in the array is a refusal**, even when `excess_amount` is `"0.00"`: the minimum-amount rule reports itself that way, and the reason sits in `error_msg`. Show the customer `available_amount` and suggest a smaller transaction. --- ## Step 5: Create the Off-Ramp Order Submit the order using the bank account UUID from Step 1. **Request:** ```bash curl https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/orders/ \ --request POST \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --header 'Idempotency-Key: order_c1d2e3f4_offramp_001' \ --data '{ "source": { "rail": "ethereum", "asset": "USDT", "amount": 500 }, "destination": { "rail": "sepa", "asset": "EUR", "client_account_uuid": "ba1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e" } }' ``` **Response:** ```json { "uuid": "od2f3a4b-5c6d-7e8f-9a0b-1c2d3e4f5a6b", "reference": "SG-20250115-GHIJKL", "state": "new", "deposit_address": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18", "source": { "rail": "ethereum", "asset": "USDT", "amount": "500.000000", "deposit_bank_info": null, "transaction_hash": null, "deposit_status": null }, "destination": { "rail": "sepa", "asset": "EUR", "amount": "473.25", "client_account": "DE89370400440532013000" }, "commission": { "asset": "USDT", "amount": "10.00" }, "account": { "uuid": "ac1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f", "code": "TRD-001", "name": "Trading Account", "type": "trading" }, "error_message": null, "third_party_document_required": false, "third_party_document_uuid": null, "created_at": "2025-01-15T10:50:00.000Z", "valid_until": "2025-01-15T11:50:00.000Z" } ``` The critical fields for off-ramp are: | Field | Value | Purpose | |-------|-------|---------| | `deposit_address` | `0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18` | Stablegate's Ethereum address where the customer sends USDT | | `reference` | `SG-20250115-GHIJKL` | Order reference (not needed for crypto deposits, but useful for your records) | | `valid_until` | `2025-01-15T11:50:00.000Z` | Deadline for the deposit | | `third_party_document_required` | `false` | Whether the order waits for an invoice (see below) | ### Payout to a Third-Party Account: Attach the Invoice In this scenario the EUR goes to the customer's own IBAN registered in Step 1, so no invoice is needed. When Stablegate treats the payout account as a third party's and the order is above 15 000 EUR (the source amount converted to EUR; the threshold may differ in your account configuration), the response has `third_party_document_required: true`. Such an order is created as usual, but it is not executed automatically until its invoice or contract is attached: after the deposit is confirmed it stays `new` and waits for Stablegate operations. Upload the file while the order is still `new`, for example before the customer sends the USDT: ```bash curl 'https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/orders/od2f3a4b-5c6d-7e8f-9a0b-1c2d3e4f5a6b/third-party-document/' \ --request POST \ --header 'X-Api-Key: YOUR_API_KEY' \ --form 'file=@invoice-2025-0142.pdf' ``` The response is the order with `third_party_document_uuid` set. Once the invoice is attached, the order is processed as usual; if the deposit has already arrived, processing resumes. The rules and errors are in [Third-Party Payouts and the Invoice](/docs/guide/orders#third-party-payouts-and-the-invoice). --- ## Step 6: Customer Sends USDT Instruct your customer to send the USDT to the deposit address: | Detail | Value | |--------|-------| | **Token** | USDT (ERC-20) | | **Network** | Ethereum mainnet | | **Recipient Address** | `0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18` | | **Amount** | `500.000000 USDT` | > **Warning:** - The customer must send USDT on the **Ethereum network** — not on other networks such as Tron or BSC. > - The amount does not have to match to the cent: an under- or overpayment changes the payout, which is recalculated from what actually arrived, and does not fail the order. > - The deposit must be completed before `valid_until`. --- ## Step 7: Monitor Order Status After the customer sends USDT, poll the order status. Ethereum transactions typically confirm within a few minutes. **Request:** ```bash curl https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/orders/od2f3a4b-5c6d-7e8f-9a0b-1c2d3e4f5a6b/ \ --header 'X-Api-Key: YOUR_API_KEY' ``` **Status transitions you will observe:** 1. **`new`** — Order created, waiting for USDT deposit. `source.deposit_status` stays `null` until the transaction is seen 2. **`new`** with `source.deposit_status: "received"` or `"confirmed"` — the transaction is on-chain and is being matched; `source.transaction_hash` carries the deposit hash the customer can check 3. **`processing`** — deposit credited, exchange in progress 4. **`done`** — EUR has been sent to the customer's bank account via SEPA **Response (completed):** ```json { "uuid": "od2f3a4b-5c6d-7e8f-9a0b-1c2d3e4f5a6b", "reference": "SG-20250115-GHIJKL", "state": "done", "deposit_address": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18", "source": { "rail": "ethereum", "asset": "USDT", "amount": "500.000000", "deposit_bank_info": null, "transaction_hash": "0x9f8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c5b4a39281706f5e4d3c2b1a0", "deposit_status": "credited" }, "destination": { "rail": "sepa", "asset": "EUR", "amount": "473.25", "client_account": "DE89370400440532013000" }, "commission": { "asset": "USDT", "amount": "10.00" }, "account": { "uuid": "ac1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f", "code": "TRD-001", "name": "Trading Account", "type": "trading" }, "error_message": null, "created_at": "2025-01-15T10:50:00.000Z", "valid_until": null } ``` > **Note:** Once the status reaches `done`, the SEPA transfer has been initiated. The customer's bank may take an additional 1 business day to credit the funds. --- ## Step 8: Order Complete The order is in the `done` state. **473.25 EUR** has been sent to the customer's IBAN (`DE89370400440532013000`) via SEPA. **Summary:** | Item | Value | |---|---| | **Customer sent** | 500.00 USDT to `0x742d...bD18` | | **Fee** | 10.00 EUR | | **Customer received** | 473.25 EUR at `DE89...3000` | | **Status** | `done` | --- ## Cancelling an Order If the customer decides not to proceed before sending the USDT deposit, you can cancel the order: **Request:** ```bash curl https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/orders/od2f3a4b-5c6d-7e8f-9a0b-1c2d3e4f5a6b/cancel/ \ --request POST \ --header 'X-Api-Key: YOUR_API_KEY' ``` **Response:** `204 No Content` After cancellation, verify the order status: ```bash curl https://api.stablegate.com/v1/customers/c1d2e3f4-a5b6-7c8d-9e0f-1a2b3c4d5e6f/orders/od2f3a4b-5c6d-7e8f-9a0b-1c2d3e4f5a6b/ \ --header 'X-Api-Key: YOUR_API_KEY' ``` ```json { "uuid": "od2f3a4b-5c6d-7e8f-9a0b-1c2d3e4f5a6b", "reference": "SG-20250115-GHIJKL", "state": "cancelled", "error_message": null, "created_at": "2025-01-15T10:50:00.000Z", "valid_until": null } ``` > **Warning:** Orders can only be cancelled before the deposit is processed. Once the order reaches `processing` status, cancellation is no longer possible. --- ## Error Handling ### Route unavailable If the route check in Step 2 returns `available: false`, inform the customer that this conversion is temporarily unavailable. ### Limits exceeded If the check in Step 4 returns a non-empty array, the transaction breaks a limit. Do not look at `excess_amount` alone: the minimum-amount rule reports itself with `excess_amount: "0.00"` and the reason in `error_msg`. Display `available_amount` and suggest a smaller transaction. ### Wrong network or token If the customer sends tokens on the wrong network (e.g., USDT on Tron instead of Ethereum) or sends a different token, the deposit will not be detected. In this case, contact the Stablegate support team for assistance. ### Order failed If the order reaches a `failed` or `rejected` state, check `error_message`. A deposit that differs in amount does not fail the order by itself: the payout is recalculated from the amount actually received. ### Order expired If the customer does not send USDT before `valid_until`, the order may expire. Create a new order if needed. --- # Common Source: https://docs.stablegate.com/docs/verification/common This section outlines the **general rules and standards** that apply to all verification procedures on the Stablegate platform — both for individuals (KYC) and corporate entities (KYB). At Stablegate, we are committed to maintaining the highest compliance standards under Swiss regulations. In line with our risk-based approach and international sanctions frameworks, we do not provide services to certain countries or high-risk business sectors. --- # Legal Notice Source: https://docs.stablegate.com/docs/verification/common/legal-notice Last updated: July 27, 2026 ## Company STGG AG (operating under the brand name "Stablegate") ## Registered office Dammstrasse 16, 6300 Zug, Switzerland ## Company registration number CHE-282.984.669 (Commercial Register of the Canton of Zug) ## Regulatory status STGG AG is a Swiss Virtual Asset Service Provider and a member of the Financial Services Standards Association (VQF), a Swiss Self-Regulatory Organisation recognised by the Swiss Financial Market Supervisory Authority (FINMA) for the purposes of the Swiss Anti-Money Laundering Act (member No. 101275). ## Represented by Boris Mozer, Director ## Contact [hi@stablegate.com](mailto:hi@stablegate.com) ## Regulatory notice STGG AG ("Stablegate") is a company incorporated under Swiss law and is not authorized under the Markets in Crypto-Assets Regulation (MiCA) or by any financial supervisory authority of the European Union or European Economic Area. The services described are not offered to EU/EEA residents except under the reverse solicitation exemption as defined in Article 61 of MiCA. --- # Terms of Business Source: https://docs.stablegate.com/docs/verification/common/terms Last updated: July 27, 2026 These Terms of Business (the “Terms”) govern the relationship between you and the Stablegate group in connection with your use of our crypto brokerage, on-/off-ramp services, OTC desk, and B2B API features available on stablegate.com and app.stablegate.com (the “Services”). --- ## 1\) Parties and contracting entity Switzerland: STGG AG, Dammstrasse 16, 6300 Zug, Switzerland; CHE-282.984.669; member of a FINMA-recognised SRO; 101275\. Contracting entity. Your contracting entity is STGG AG, subject to applicable law and any product-specific requirements. STGG AG may serve clients in any permitted jurisdiction (excluding restricted countries). We will confirm the contracting entity during onboarding and/or in your order/trade confirmation. Contact: [hi@stablegate.com](mailto:hi@stablegate.com) **Regulatory Disclosure.** STGG AG ("Stablegate") is a company incorporated under Swiss law and is not authorized under the Markets in Crypto-Assets Regulation (MiCA) or by any financial supervisory authority of the European Union or European Economic Area. The services described are not offered to EU/EEA residents except under the reverse solicitation exemption as defined in Article 61 of MiCA. --- ## 2\) Eligibility * You confirm that you are at least 18 years old and have the capacity to enter into these Terms. * Our Services are not offered to U.S. residents or citizens or persons otherwise prohibited by applicable sanctions/embargoes. * You may only use the Services if permitted by the laws of your country of residence/domicile. **Reverse Solicitation Affirmation.** If you are a natural person who is a resident or citizen of the European Union (EU) or European Economic Area (EEA), or if you are a legal entity established or registered in the EU/EEA, you confirm that you are engaging with STGG AG ("Stablegate") entirely at your own initiative and not as a result of any direct or indirect marketing, solicitation, or advertising by STGG AG ("Stablegate") or any of its representatives. You further acknowledge that Stablegate's services are made available to you solely under the reverse solicitation exemption as defined in Article 61 of the Markets in Crypto-Assets Regulation (MiCA). --- ## 3\) Scope of Services * On-/Off-Ramp Exchange. Fiat ↔ crypto exchange and payouts. * OTC Desk. Quote-driven execution for larger or bespoke orders. * B2B API. Enterprise access to exchange and orchestration features, subject to Section 12\. * No margin/derivatives/staking/lending. We do not provide these products. * No custody. We do not hold client assets on a custodial basis. Client funds/crypto are received solely to execute an exchange and promptly transferred to the destination you designate (Section 5). ### Cross-border Services Restrictions STGG AG is a Swiss Virtual Asset Service Provider, member of the Financial Services Standards Association (VQF), a Swiss Self-Regulatory Organisation under the supervision of Swiss Financial Market Supervisory Authority (FINMA). STGG's products and services are only regulated in Switzerland. They are not registered or approved outside of Switzerland. Any information on STGG's website [https://stablegate.com/](https://stablegate.com/) (i) is not intended and should not be construed as an offer, solicitation or recommendation outside of Switzerland and (ii) may not be suitable or lawful in your jurisdiction. You are welcome to explore STGG's website at your own initiative. Click 'Proceed at My Own Initiative' if you are either (i) a resident of, or domiciled in, Switzerland, or (ii) are resident of, or domiciled outside of, Switzerland but would like to visit STGG's site on your own initiative. If you fall under (ii) above, when choosing 'Proceed at My Own Initiative', it is important to do this on your own initiative, as STGG is not authorized to promote its products and services outside Switzerland. If you do not wish to visit the site, please click 'Exit Site'. If you click 'Proceed at My Own Initiative' to explore, please note that you: * are confirming you have read and understood the above; * are accessing STGG's website at your own initiative, without active promotion or solicitation from STGG; * are responsible for ensuring access is lawful in your jurisdiction; and * understand that exploring the website does not create a customer relationship. --- ## 4\) Onboarding, KYC/AML and compliance * You must complete onboarding and provide accurate information, and you consent to our KYC/AML checks, including liveness and other identity-verification steps performed by our service providers. * We may request updated documents at any time and you agree to provide them promptly. * We may share Travel Rule information with other VASPs and reject, block, or delay transfers that lack required originator/beneficiary information or that otherwise present compliance risk. --- ## 5\) Funding, withdrawals and third-party payments * Deposits (fiat/crypto). You may fund your orders from your own accounts/wallets or from permitted third parties where legally allowed. We may request documentation to establish the third party’s identity/relationship and the source of funds/wealth. * Withdrawals/payouts. We send fiat/crypto to the destination you designate (your own or a permitted third party’s). We may withhold or route transfers differently to meet legal requirements, mitigate fraud, or comply with network/payment-scheme rules. * No formal min/max. We do not impose general minimums/maximums. Operational, regulatory, or partner constraints may require temporary or per-transaction limits, which we will communicate where practicable. * Network confirmations & fees. On-chain transfers require network confirmations and may incur network fees; settlement times depend on the underlying network or payment system. * Unsupported assets. We may decline to accept or withdraw tokens/networks we do not support. If you send unsupported assets, recovery may be impossible or subject to recovery fees and best efforts only. --- ## 6\) Quotes, execution and pricing * Quote before funding. We will inform you of the exchange rate and fees before you initiate funding. * Validity. Unless otherwise stated, quotes are valid for 1 hour from issuance and may be conditional on receipt of funds within that window; if funds are not received in time, we may re-quote. * Best effort/best execution. We use reasonable care, skill, and best efforts to achieve competitive pricing through our liquidity sources (exchanges/OTC counterparties). Actual execution may differ from indicative quotes due to market movement, latency, or venue conditions. * Slippage and partial fills. You accept the risk of slippage and that we may complete orders in multiple fills/venues. For OTC, a trade confirmation will specify the final executed rate/amount. * FX handling. Fiat currency conversions (if any) use rates provided by our partners at the time of execution. --- ## 7\) OTC desk (additional terms) * OTC trades are principal or riskless principal transactions executed against our or our partner liquidity. * We may require pre-funding or collateral. Settlement, cut-offs, and instructions are as per the trade confirmation. * Orders may be cancelled if market disruption or extraordinary volatility occurs. --- ## 8\) No custody; title and risk * We do not operate a wallet or custodial service and do not take ongoing possession of client assets. * Any temporary holding is incidental to execution and not a deposit, investment, or safekeeping service, and does not earn interest, yield, or staking rewards. * Title and risk in crypto assets pass upon broadcast/confirmation to your designated address; for fiat, upon our bank’s acceptance of your outgoing payment order (subject to payment-scheme rules). --- ## 9\) Fees Our fees are published on a separate page: [Fees](/docs/guide/fees-and-limits/fees). Fees may include spreads, commissions, network/partner fees, and recovery or investigation costs where applicable. --- ## 10\) Prohibited and restricted activity You agree not to use the Services to engage in illegal activity, sanctions evasion, market manipulation, fraud, IP infringement, or activity that would expose Stablegate or its partners to regulatory or security risk. We may restrict assets, addresses, counterparties, or geographies as required by law or our risk policies. --- ## 11\) Account suspension, freezing and termination We may freeze, suspend, or terminate your access, or delay/decline a transaction if: (a) you breach these Terms; (b) you fail to provide up-to-date documents on request; (c) we detect fraud, suspicious activity, or regulatory risk; (d) required by law, court, regulator, or SRO; or (e) the account is inactive for more than 12 months. Where legally permitted, we will notify you. We are not obliged to explain the reason for a freeze, suspension, termination, or declined transaction. --- ## 12\) API terms (B2B) * Licence. We grant you a non-exclusive, non-transferable, revocable licence to use our API solely to integrate the Services into your systems. * Keys & security. You must keep API keys confidential, implement industry-standard security, and notify us immediately of compromise. * Rate limits & fair use. You must comply with rate limits and avoid undue load or abusive calling patterns. * Uptime/SLA. We provide the API on a commercially reasonable efforts basis; no guaranteed uptime unless agreed in writing. * Data. You may not use data from the Services to build competing services, for surveillance, or in violation of privacy laws. * Sandbox. Test data must not include real personal data unless we agree in writing. --- ## 13\) Forks, airdrops and protocol events We are not obliged to support forks, airdrops, re-orgs, token migrations, or network upgrades. We may suspend processing during such events and determine, at our discretion, whether and how to support resulting assets. --- ## 14\) Risk disclosures Crypto assets involve market volatility, technology and smart-contract risk, liquidity risk, operational and custody risk (even though we are non-custodial, execution involves third-party venues), regulatory changes, stablecoin de-pegs, and blockchain finality considerations. You should consider whether you can afford the risk of loss. You are responsible for your tax obligations. **Acceptance of Risk.** A Client guarantees to comply with and act within all legal boundaries within the applicable jurisdiction to the business relationship, particularly in relation to MiCA regulations applicable in the EU/EEA, and especially including but not limited to tax laws. --- ## Our Security Obligations STGG AG ("Stablegate") implements robust technical and organizational measures to safeguard client information and digital assets. These include secure encryption, multi-factor authentication, ongoing vulnerability assessments, and infrastructure-level intrusion detection systems. These measures complement client-side security responsibilities outlined in this document. --- ## 15\) Communications and notices You consent to receive electronic communications (in-app, email). Notices are deemed received when sent to your registered email address or delivered in-app. You must keep your contact details current. --- ## 16\) Intellectual property The Services, site/app, API, and content are protected by IP laws. We grant a limited licence to access and use the Services for their intended purpose. You may not copy, modify, reverse engineer, or scrape the Services except as permitted by law. **Not for Distribution.** This document is confidential and is intended only for persons who have contacted STGG AG ("Stablegate") at their own initiative. It may not be copied, distributed, published, or shared - particularly in jurisdictions such as the European Union (EU) or European Economic Area (EEA), where STGG AG ("Stablegate") does not offer or advertise its services. --- ## 17\) Liability and indemnity * No indirect damages. To the extent permitted by law, we are not liable for indirect, incidental, special, exemplary, or consequential damages, loss of profits, data, goodwill, or business interruption. * Cap. Our aggregate liability for the Services in any 12-month period is capped at the fees you paid to us for the relevant Services in that period, except for liability that cannot be limited by law (e.g., fraud, wilful misconduct). * Indemnity. You will indemnify us and our partners against losses arising from your breach of these Terms or violation of law, except to the extent caused by our fraud or wilful misconduct. --- ## 18\) Changes to the Services and to these Terms We may modify the Services or these Terms. Material changes will be notified via the site/app or email with reasonable advance notice where practicable. Continued use after the effective date constitutes acceptance. --- ## 19\) Governing law and venue These Terms are governed by Swiss law; the exclusive venue is the courts of Zug, Switzerland (subject to mandatory consumer jurisdiction rules, if applicable). --- ## 20\) Language These Terms are provided in English only. If we provide translations, the English version controls in case of conflict to the extent permitted by law. --- ## 21\) Contact Stablegate — Privacy & Legal [hi@stablegate.com](mailto:hi@stablegate.com) STGG AG — Dammstrasse 16, 6300 Zug, Switzerland; CHE-282.984.669; member of a FINMA-recognised SRO; 101275\. --- # Privacy Policy Source: https://docs.stablegate.com/docs/verification/common/privacy Last updated: July 27, 2026 This Privacy Policy explains how the Stablegate group processes personal data in connection with its crypto brokerage, on-/off-ramp, and stablecoin orchestration services, including our B2B API features, on stablegate.com and app.stablegate.com (the “Services”). --- ## 1\) Who we are (Controller) Switzerland (FADP): STGG AG — Dammstrasse 16, 6300 Zug, Switzerland; CHE-282.984.669; member of a FINMA-recognised SRO; 101275\. STGG AG acts as the controller for personal data processed in connection with the Services. If you have questions about the processing of your data, contact us using Section 17\. Contact: [hi@stablegate.com](mailto:hi@stablegate.com) Service availability: Our Services are not intended for residents or citizens of the United States. Availability may also be restricted where prohibited by law. --- ## 2\) Scope & who this policy covers This policy covers: retail clients, corporate clients (including financial institutions), and affiliates using our Services. It does not cover job applicants or employees/contractors (handled under separate notices where applicable). --- ## 3\) What personal data we collect We collect only what we need to deliver the Services, meet legal duties, and protect users. The data we process depends on how you interact with us: * Identification & KYC/AML: name, date/place of birth, nationality, address, identity document details and images, liveness checks performed by a third-party, results of sanctions/PEP screening, adverse-media results, beneficial-owner and company-registry data, source of funds/wealth and supporting documentation. * Financial & transactional: fiat/crypto payment details, bank references, wallet/public addresses, transaction histories, payment references and memos, counterparties. * Risk & compliance: risk scores, watchlist matching outcomes, blockchain analytics outputs, fraud flags. * Technical & usage: IP address, device/OS/browser data, app/web telemetry, event logs, session data, cookies/SDKs (see Cookies). * Support & communications: emails, tickets, chat transcripts; call recordings where permitted. * Marketing preferences (if you opt in). Note on blockchains. Certain transaction data (e.g., transfers to/from your public addresses) is recorded on public, append-only ledgers outside our control. We minimise linking on-chain data to identity beyond what is necessary for legal and operational purposes. Corporate users/B2B API. For enterprise customers we process business-contact data of your staff (names, work emails, roles) and API usage/log data to operate, secure, and bill the Services. --- ## 4\) Why we process your data (purposes) and legal bases Contract & legitimate interests (GDPR Art. 6(1)(b), (f); FADP principles): * Provide, operate, and improve the Services; onboarding, account management, customer support. * Detect, prevent, and investigate fraud/abuse; protect platform integrity and network security. * Service analytics and product improvement (minimised/aggregated where feasible). Legal obligations (GDPR Art. 6(1)(c) where applicable; Swiss AMLA): * KYC/AML/CTF due diligence, sanctions/PEP screening, ongoing monitoring, suspicious-activity handling, required reporting, and audit trails. * Record-keeping and responding to lawful requests from competent authorities and SROs. Consent (GDPR Art. 6(1)(a)) — used where required by law: * Optional marketing communications; certain analytics/marketing cookies (see Cookies). Biometrics & liveness. * Where we carry out liveness and face-matching, we rely on applicable anti-money laundering laws (substantial public interest where recognised) and/or your explicit consent where required by local law. We do not use biometric data for any purpose other than identity verification and fraud prevention. --- ## 5\) Automated decisions & profiling We use screening and risk models (e.g., sanctions, PEP, fraud, blockchain analytics) that may affect onboarding, transaction handling, or feature access. Where applicable law grants you rights, you may request human review, express your point of view, and contest an automated decision. We design controls to avoid unfair bias and test models periodically. --- ## 6\) Sources of data * You (and your devices) when you sign up, transact, or contact support. * Third parties such as KYC/AML providers, sanctions/adverse-media databases, public company/beneficial-ownership registers, and blockchain-analytics providers. * Financial partners involved in fiat/crypto on- and off-ramps (limited to what is needed to execute transactions and comply with law). --- ## 7\) Disclosures (who we share data with) * Processors/service providers (hosting, KYC/AML/analytics, communications, customer support, cloud security, payments). We require confidentiality, appropriate security, and processing only on our instructions. * Financial partners and payment networks to execute on/off-ramp transactions. * Affiliates within the Stablegate group for the purposes stated in this policy. * Other VASPs (Travel Rule). Where required, we exchange originator/beneficiary information with other virtual-asset service providers to comply with the Travel Rule or equivalent rules; we may reject or block transfers lacking required data. * Authorities / SRO / regulators when required by law or to protect users, our platform, or others. * Corporate customers (B2B). For enterprise features we may disclose usage or security information to your organisation’s administrators consistent with our contract. We do not sell your personal data. --- ## 8\) International transfers We process data in Switzerland and the European Economic Area (EEA). Where we transfer personal data to countries without an adequate level of protection, we implement safeguards such as EU Standard Contractual Clauses (SCCs) (with Swiss addenda where needed) and additional measures proportionate to the risks. Copies of key clauses can be provided on request, subject to redactions. --- ## 9\) Retention We retain personal data only as long as necessary for the purposes above and to meet legal obligations. Unless a longer period is required by law or necessary to establish/exercise/defend legal claims, we apply the following defaults: * KYC/AML records: 10 years after relationship end/transaction completion. * Accounting/financial records: 10 years from fiscal year-end. * Support tickets & communications: 3 years after closure. * Access, authentication and audit logs: 10 years. * Marketing lists: until you opt out or after 24 months of inactivity. * Biometric templates/liveness artefacts: retained only for verification and dispute purposes, then deleted; we aim to minimise storage and use deletion or irreversible pseudonymisation where possible. Where retention periods differ by law, we apply the longer legally required period. --- ## 10\) Your rights EU/EEA (GDPR, where applicable): You have rights of access, rectification, erasure, restriction, portability, objection, and rights related to automated decision-making (including profiling). Switzerland (FADP): You have rights to information and other protections aligned with the controller’s transparency duties. How to exercise: Contact [hi@stablegate.com](mailto:hi@stablegate.com). We may request proof of identity. You can also lodge a complaint with your local supervisory authority or consult the FDPIC in Switzerland. --- ## 11\) Security We maintain technical and organisational measures appropriate to financial-services risk, including encryption in transit/at rest, access controls, segregation, logging/monitoring, incident response, and vendor due diligence. Staff access is limited on a need-to-know basis and protected by authentication controls. We test and improve controls periodically. --- ## 12\) Cookies & similar technologies We use necessary cookies to operate the Services and (where required by law) consent-based analytics/marketing cookies. For details—including cookie names, purposes, and durations—see our [Cookie Notice](/docs/verification/common/cookie-notice) and on-site preferences banner. You can change your cookie settings at any time via the banner or your browser settings. **Cookie & Geo-Tracking Limitation.** STGG AG ("Stablegate") uses cookies and web analytics tools to optimize performance and security. We do not use these tools to market services to users in the EU/EEA. IP address and geolocation data may be processed to restrict access from jurisdictions where STGG AG ("Stablegate") is not authorized, including the EU/EEA. **Regulatory Disclosure.** STGG AG ("Stablegate") is a company incorporated under Swiss law and is not authorized under the Markets in Crypto-Assets Regulation (MiCA) or by any financial supervisory authority of the European Union or European Economic Area. The services described are not offered to EU/EEA residents except under the reverse solicitation exemption as defined in Article 61 of MiCA. --- ## 13\) Children Our Services are not directed to individuals under 18 (or the age of majority in your jurisdiction if higher). We do not knowingly collect their data. If you believe a child has provided data, contact us so we can delete it unless retention is required by law. --- ## 14\) Product-specific transparency for crypto * Public ledgers: Transactions may be publicly accessible and immutable. While we minimise linkability, publication on the ledger is inherent to the Services. * Risk controls: We use sanctions/PEP lists, adverse-media, and blockchain analytics to comply with AML/CTF obligations and prevent fraud. * Transaction screening/blocks: To protect users and the platform, we may delay, decline, or block transactions that fail checks or would breach law or our policies; you can request human review where required. --- ## 15\) Switzerland-specific information (STGG AG) * SRO & AML duties: As a Swiss financial intermediary, STGG AG performs customer due diligence, ongoing monitoring, and reporting required by AMLA/AMLO and SRO rules. * Representative: Not required because STGG AG is established in Switzerland. * Retention: See Section 9 (Swiss-specific 10-year duties). --- ## 16\) Breach notifications * GDPR: We notify the competent supervisory authority without undue delay and, where feasible, within 72 hours after becoming aware of a personal-data breach, unless the breach is unlikely to result in risk to individuals; where risk is high, we also inform affected individuals. * Swiss FADP: We notify the FDPIC as soon as possible when a high-risk data-security breach occurs; we may also notify affected individuals when necessary. --- ## 17\) Contact us Privacy contact: [hi@stablegate.com](mailto:hi@stablegate.com) * STGG AG — Dammstrasse 16, 6300 Zug, Switzerland; CHE-282.984.669; member of a FINMA-recognised SRO; 101275\. --- ## 18\) Changes to this Policy We will post updates here and adjust the “Last updated” date. For material changes, we will provide additional notice where legally required. --- ## 19\) Cross-border and Reverse Solicitation notices ### Cross-border Services Restrictions STGG AG is a Swiss Virtual Asset Service Provider, member of the Financial Services Standards Association (VQF), a Swiss Self-Regulatory Organisation under the supervision of Swiss Financial Market Supervisory Authority (FINMA). STGG's products and services are only regulated in Switzerland. They are not registered or approved outside of Switzerland. Any information on STGG's website [https://stablegate.com/](https://stablegate.com/) (i) is not intended and should not be construed as an offer, solicitation or recommendation outside of Switzerland and (ii) may not be suitable or lawful in your jurisdiction. ### Reverse Solicitation Affirmation If you are a natural person who is a resident or citizen of the European Union (EU) or European Economic Area (EEA), or if you are a legal entity established or registered in the EU/EEA, you confirm that you are engaging with STGG AG ("Stablegate") entirely at your own initiative and not as a result of any direct or indirect marketing, solicitation, or advertising by STGG AG ("Stablegate") or any of its representatives. You further acknowledge that Stablegate's services are made available to you solely under the reverse solicitation exemption as defined in Article 61 of the Markets in Crypto-Assets Regulation (MiCA). --- # Reverse Solicitation Policy Source: https://docs.stablegate.com/docs/verification/common/reverse-solicitation-policy Last updated: August 25, 2026 STGG AG ("Stablegate") is a company incorporated under Swiss law and is not authorized under the Markets in Crypto-Assets Regulation (MiCA) or by any financial supervisory authority of the European Union or European Economic Area. The services described are not offered to EU/EEA residents except under the reverse solicitation exemption as defined in Article 61 of MiCA. --- ## Cross-border Services Restrictions STGG AG is a Swiss Virtual Asset Service Provider, member of the Financial Services Standards Association (VQF), a Swiss Self-Regulatory Organisation under the supervision of Swiss Financial Market Supervisory Authority (FINMA). STGG's products and services are only regulated in Switzerland. They are not registered or approved outside of Switzerland. Any information on STGG's website [https://stablegate.com/](https://stablegate.com/) (i) is not intended and should not be construed as an offer, solicitation or recommendation outside of Switzerland and (ii) may not be suitable or lawful in your jurisdiction. You are welcome to explore STGG's website at your own initiative. Click 'Proceed at My Own Initiative' if you are either (i) a resident of, or domiciled in, Switzerland, or (ii) are resident of, or domiciled outside of, Switzerland but would like to visit STGG's site on your own initiative. If you fall under (ii) above, when choosing 'Proceed at My Own Initiative', it is important to do this on your own initiative, as STGG is not authorized to promote its products and services outside Switzerland. If you do not wish to visit the site, please click 'Exit Site'. If you click 'Proceed at My Own Initiative' to explore, please note that you: * are confirming you have read and understood the above; * are accessing STGG's website at your own initiative, without active promotion or solicitation from STGG; * are responsible for ensuring access is lawful in your jurisdiction; and * understand that exploring the website does not create a customer relationship. --- ## Marketing Abstention STGG AG ("Stablegate") does not direct any marketing, advertising or promotional activity at the European Union (EU) or the European Economic Area (EEA). In particular, Stablegate: 1. does not attend, sponsor or present at crypto-asset or fintech events in the EU/EEA, except where it is made clear that no services are offered to EU/EEA residents; 2. does not maintain any mandate with agents, introducers or other third parties for the purpose of introducing clients established or situated in the EU/EEA; and 3. does not send marketing or promotional material to its existing EU/EEA clients. Communications with such clients are limited to service, transactional and legally required messages relating to the services they have themselves requested. --- ## Reverse Solicitation Affirmation If you are a natural person who is a resident or citizen of the European Union (EU) or European Economic Area (EEA), or if you are a legal entity established or registered in the EU/EEA, you confirm that you are engaging with STGG AG ("Stablegate") entirely at your own initiative and not as a result of any direct or indirect marketing, solicitation, or advertising by STGG AG ("Stablegate") or any of its representatives. You further acknowledge that Stablegate's services are made available to you solely under the reverse solicitation exemption as defined in Article 61 of the Markets in Crypto-Assets Regulation (MiCA). The full client declaration is set out in the [Reverse Solicitation Declaration](/docs/verification/common/reverse-solicitation-declaration), which EU/EEA clients accept at registration. --- ## Not for Distribution This document is confidential and is intended only for persons who have contacted STGG AG ("Stablegate") at their own initiative. It may not be copied, distributed, published, or shared - particularly in jurisdictions such as the European Union (EU) or European Economic Area (EEA), where STGG AG ("Stablegate") does not offer or advertise its services. --- # Reverse Solicitation Declaration Source: https://docs.stablegate.com/docs/verification/common/reverse-solicitation-declaration Last updated: July 27, 2026 This Reverse Solicitation Declaration applies where a client is resident, domiciled, established or situated in the European Union or European Economic Area, or otherwise accesses the services of STGG AG from such territory. For the purposes of this Declaration, “Stablegate” means STGG AG, a Swiss company with registration number CHE-282.984.669 and legal address at Dammstrasse 16, 6300 Zug, Switzerland, operating under the brand name Stablegate. By accepting this Declaration, I confirm, represent and warrant, on my own behalf and, where applicable, on behalf of the legal entity I represent, that: --- ## Own exclusive initiative I am contacting Stablegate and submitting this registration request entirely on my own exclusive initiative. I understand that registration creates only a restricted profile for compliance and onboarding review purposes. Registration does not, by itself, give me access to any crypto-asset service, payment-related service, product, functionality or other regulated service of Stablegate. --- ## No solicitation Before and in connection with submitting this registration request, I was not solicited, approached, targeted, encouraged, invited, induced or otherwise prompted, directly or indirectly and regardless of the means of communication used, by Stablegate to open an account or obtain any crypto-asset service, payment service or other service. I further confirm that I was not contacted for such purpose by any affiliated entity, employee, representative, agent, introducer, partner, consultant, influencer, affiliate or any other person acting directly or indirectly on behalf of Stablegate. --- ## No marketing, referral or inducement My decision to access Stablegate’s website, submit a registration request and/or request services did not result from, and was not influenced by, to the best of my knowledge and belief: * any advertisement, marketing campaign, sponsored content, paid search result, sponsored placement or other promotional communication by or on behalf of Stablegate; * any email, telephone call, message, social media communication, newsletter, push notification or other direct communication from Stablegate promoting its services; * any referral arrangement, affiliate programme, introducer arrangement, influencer communication, commission-based recommendation or other incentive scheme connected with Stablegate; * any invitation to an event, roadshow, training session, webinar, meeting or other promotional activity organised by or on behalf of Stablegate; * any recommendation, redirection or introduction made by a person acting on behalf of, or having a commercial relationship with, Stablegate; * any redirection, backlink, logo placement or promotional reference to Stablegate on the website, application, platform, educational material or communication of an affiliated entity, business partner or other third party acting on behalf of or having a commercial relationship with Stablegate. --- ## Independent identification of Stablegate I independently identified Stablegate and independently decided to seek information about its services, without being redirected, introduced or encouraged to do so by any person acting on behalf of, having close links with, or having a commercial relationship with Stablegate. Where requested, I will provide a complete and accurate description of how I became aware of Stablegate. --- ## Scope of requested services This Declaration records that I am contacting Stablegate and submitting this registration request on my own exclusive initiative. I understand that registration is limited to the creation of a restricted profile and does not constitute a general request for access to all services, products, crypto-assets, payment-related services or functionalities of Stablegate. I understand that, before any crypto-asset service, payment-related service, product or functionality is made available to me, I may be required during onboarding to identify the specific service(s), activity or activities, crypto-asset category or categories, payment-related service(s), functionality or product(s) that I independently request from Stablegate. I understand that only the service(s), activity or activities, crypto-asset category or categories, payment-related service(s), functionality or product(s) specifically requested by me and approved by Stablegate may be made available to me. I understand that this Declaration does not constitute a general, standing or future request for any other crypto-asset service, payment service, product, token, trading functionality, investment service, additional feature or future service that has not been specifically requested by me. I understand that my own exclusive initiative does not authorise Stablegate to market new types of crypto-assets, crypto-asset services or other regulated services to me. If I later request any additional, materially different or new type of service, product, crypto-asset category, token or functionality, I understand that Stablegate may require a separate assessment and, where appropriate, a separate declaration or confirmation before making such service available. --- ## Regulatory acknowledgement I acknowledge that STGG AG is established in Switzerland and operates within the scope of its Swiss regulatory status. STGG AG is a member of VQF SRO, a FINMA-recognised Swiss self-regulatory organisation for anti-money laundering supervision purposes. I understand that such Swiss regulatory status is not equivalent to authorisation as a crypto-asset service provider under Regulation (EU) 2023/1114 on markets in crypto-assets (MiCA), or to authorisation as a payment institution, electronic money institution or payment service provider in any EU or EEA Member State under Directive (EU) 2015/2366 on payment services in the internal market (PSD2). I acknowledge that my request to access the services of Stablegate is made on my own exclusive initiative. --- ## Record-keeping I understand and agree that this Declaration, together with related registration, onboarding, communication, technical, verification and compliance records, may be retained by Stablegate for legal, regulatory, compliance, audit and evidentiary purposes. Such records may include, where applicable, timestamps, IP address, approximate location data, device and browser information, user-agent data, referral URL, UTM or campaign data, traffic-source data, service-selection records, version history of accepted documents, onboarding questionnaire responses, service-selection records, onboarding provider records, communication records and verification records, subject to applicable data protection law and Stablegate’s Privacy Policy and Cookie Policy. I acknowledge that Stablegate may rely on this Declaration and related records as evidence that I initiated the relationship and requested the relevant services on my own exclusive initiative. --- ## Accuracy and continuing obligation I confirm that the information provided in this Declaration is true, complete and not misleading. I confirm that I have not been instructed, encouraged or assisted by Stablegate or by any person acting on behalf of Stablegate to provide a false, incomplete or misleading declaration regarding how I became aware of Stablegate or why I am requesting its services. I undertake to notify Stablegate promptly if any information provided in this Declaration becomes inaccurate, incomplete or misleading. --- # Cookie Notice Source: https://docs.stablegate.com/docs/verification/common/cookie-notice Last updated: July 27, 2026 STGG AG ("Stablegate") uses cookies and web analytics tools to optimize performance and security. We do not use these tools to market services to users in the EU/EEA. IP address and geolocation data may be processed to restrict access from jurisdictions where STGG AG ("Stablegate") is not authorized, including the EU/EEA. STGG AG ("Stablegate") is a company incorporated under Swiss law and is not authorized under the Markets in Crypto-Assets Regulation (MiCA) or by any financial supervisory authority of the European Union or European Economic Area. The services described are not offered to EU/EEA residents except under the reverse solicitation exemption as defined in Article 61 of MiCA. Cross-border access restrictions and reverse solicitation terms apply to the use of this website - see our [Reverse Solicitation Policy](/docs/verification/common/reverse-solicitation-policy) and [Terms of Business](/docs/verification/common/terms). --- ## Cookies we use * Necessary cookies. Required to operate the website and the Services: session management, security, load balancing, and remembering your cookie preferences. These cookies are always active. * Analytics cookies (consent-based). Used, with your consent where required by law, to understand how the website is used and to improve performance and security. We do not use analytics cookies to market our services to users in the EU/EEA. --- ## Managing cookies You can change your cookie settings at any time via the on-site preferences banner or your browser settings. Blocking necessary cookies may affect the operation of the website. --- # Non-Serviced Countries Source: https://docs.stablegate.com/docs/verification/common/countries Stablegate does **not** onboard or process transactions for users or entities registered in, residing in, or transacting from the following jurisdictions: - Afghanistan - Angola - Belarus - Bolivia - Burundi - Central African Republic - Congo (Democratic Rep.) - Côte d’Ivoire - Democratic People’s Republic of Korea - Guatemala - Guinea - Guinea Bissau - Haiti - Iran - Iraq - Laos - Lebanon - Libya - Mali - Moldova - Myanmar - Namibia - Nepal - Nicaragua - Papua New Guinea - Russia - Somalia - South Sudan - Sudan - Syria - Ukraine (occupied areas) - USA - Venezuela - Yemen - Zimbabwe ## Note This list may be updated periodically in accordance with international sanctions and regulatory guidelines. --- # Non-Serviced Business Sectors Source: https://docs.stablegate.com/docs/verification/common/business The following industries and business types are **not supported** on the Stablegate platform: * Military, defense, or arms trade * Trade in exotic or protected animals * Charities, NGOs, and non-profit organizations ## Important Stablegate reserves the right to reject or terminate accounts engaged in, or associated with, any restricted jurisdiction or business activity listed above. --- # Document Requirements Source: https://docs.stablegate.com/docs/verification/common/documents All submitted documents for verification must be: * **Clear, complete, and high-quality** (no glare, cropping, or blurred text) * Written in **English** or in one of the **official Swiss languages** — **German**, **French**, or **Italian** If your documents are in another language, please include a **Google Translate version** alongside the original. Stablegate reserves the right to request an **official certified translation** if needed for compliance review. --- # Compliance Source: https://docs.stablegate.com/docs/verification/compliance ## Overview **Compliance** at Stablegate is the foundation of trust, transparency, and regulatory integrity across all services of the platform. Our compliance framework ensures that every client — whether individual or corporate — operates within the highest standards of international financial regulation while enjoying a seamless digital experience. The Stablegate platform embeds compliance directly into its architecture, enabling automatic verification, continuous monitoring, and transparent reporting. Through this integrated approach, compliance becomes not an obstacle, but a key enabler of secure and scalable financial operations. ## Compliance Framework in Stablegate The Stablegate Compliance Layer connects three fundamental components: 1. **Identity Verification (KYC/KYB)** Verifies the authenticity of each client — individual or corporate — using automated checks, biometric liveness verification, and document validation. 2. **Transaction Monitoring** Tracks all asset movements — fiat and crypto — across the platform to detect unusual patterns and prevent money laundering or fraud. 3. **Risk & Reporting Controls** Applies internal policies, risk scoring models, and audit-ready data retention to ensure compliance with Swiss AML and EU financial directives. These layers are natively integrated with the API, so partners can perform verification, risk checks, and compliance reporting directly within their own user flows. ## KYC & KYB Verification Principles KYC (Know Your Customer) and KYB (Know Your Business) form the entry point to the Stablegate ecosystem. Before a client can trade, transfer, or hold assets, their identity and the legitimacy of their funds are verified. | Process | Purpose | Key Elements | |----------|----------|--------------| | **Individual KYC** | Confirm a personal user’s identity and residence | ID document, selfie (liveness), proof of address, compliance questionnaire | | **Corporate KYB** | Validate a company’s structure and authorized representatives | Company documents, shareholder structure, representative ID, business questionnaire | Verification is fully digital and can be completed in minutes. Activation occurs automatically once compliance approval is granted. ## Source of Funds (SoF) and Continuous Monitoring After initial verification, Stablegate ensures ongoing compliance through: - **Automated screening** against global sanctions, PEP, and adverse media lists. - **Source of Funds (SoF)** checks for higher-tier accounts or large transactions. - **Real-time monitoring** of both fiat and blockchain transactions. - **Periodic re-verification** based on risk profile and jurisdiction. This continuous approach allows the platform to balance regulatory requirements with user convenience. ## Role of Compliance in the Digital Platform In the Stablegate architecture, compliance is not a standalone module — it’s a **core service** connected to every API layer: - Every client record links to a **Legal Entity** and **Compliance Status**. - Every transaction or withdrawal passes through **risk checks**. - Every order or conversion is executed only for **verified entities**. - The compliance layer provides **audit logs and transparency** for regulators and partners. Through this design, Stablegate ensures that compliance is **automated, verifiable, and audit-ready** by default. ## Guiding Principles Stablegate’s compliance model is built on four key principles: 1. **Regulatory Alignment** — Full adherence to Swiss AML laws, FATF recommendations, and EU directives. 2. **Transparency** — Clear data flows and traceability across every operation. 3. **Security by Design** — End-to-end encryption, role-based access, and segregated data storage. 4. **User Experience** — Compliance should be intuitive, fast, and frictionless. **In essence**, compliance in Stablegate is not just a legal requirement — it’s the **trust layer** of the entire ecosystem, enabling secure interaction between people, businesses, and digital assets. --- # Individual Verification (KYC) Source: https://docs.stablegate.com/docs/verification/compliance/individual At **Stablegate**, identity verification (KYC) ensures secure transactions and compliance with **Swiss** regulations. This process confirms who you are, helps prevent fraud, and protects the integrity of the platform. **Before completing any verification, you cannot send or receive funds** — registering and entering your full name does not by itself unlock transacting (monthly limit: €0). Verification does not start on its own: you start it from your Stablegate account after registration, or the partner that registered you starts it through the Partner API. There are two verification levels for individuals: **Instant** (also referred to as *Light*) and **Standard** (full verification). ## Instant Verification Instant Verification asks only for your **first name**, **last name**, and **date of birth**. No documents, selfie, or deposit are required. After you submit these details, Stablegate runs an automated AML screening (sanctions and politically exposed persons). If the screening is clear, the application is **approved automatically within minutes**. If the screening returns a match, our compliance team reviews the application manually. Once approved, you can use your account with a **monthly transaction limit of up to €1,000**. At this stage, you may only send and receive funds **to and from your own bank accounts** (the account holder name must exactly match your Stablegate profile name). ## Standard Verification To increase your transaction limits and unlock full access to Stablegate features, complete **Standard Verification**: an ID document, a selfie (liveness check), proof of address, and a short questionnaire — see [Verification Process](/docs/verification/compliance/individual/verification). After approval, the account is activated with a test deposit or a video interview — see [Account Activation](/docs/verification/compliance/individual/activation). See [Limits](/docs/guide/fees-and-limits/limits) for the limits that apply at each level. ## Verification via the Partner API Customers created through the [Partner API](/docs/guide/customers) can be verified at either level: * **Instant** — the partner submits the customer's first name, last name, and date of birth to the instant verification endpoint; the AML screening and approval then run exactly as described above. * **Standard** — the partner requests a verification link and redirects the customer to the hosted verification flow. See [Customer Verification](/docs/guide/customers#customer-verification) in the Customers guide for the endpoints. --- # Verification Process Source: https://docs.stablegate.com/docs/verification/compliance/individual/verification The entire process usually takes **under 5 minutes**, and approval is typically completed within **a few minutes**. During verification, you will be asked to provide: ## 1. Upload your ID document We accept government-issued photo IDs that contain a visible **MRZ code (Machine-Readable Zone)** — the two or three lines of letters and numbers printed at the bottom of your passport or ID card. The MRZ code allows our system to automatically and securely read key details for faster verification. ### Accepted IDs * Passport (preferred) * National ID card * Residence permit * Driver’s license (only if it includes an MRZ code) You can **photograph or upload a scan** of your document. Ensure good lighting, all corners visible, and all information clearly readable. Blurry, cropped, or edited images will not be accepted. ## 2. Take a selfie (liveness check) To confirm that the ID belongs to you, the system will ask you to take a **selfie** directly within the verification flow. Follow the on-screen instructions — make sure your face is clearly visible, without hats, sunglasses, or strong backlight. This helps ensure you are a real person and that your ID matches your photo. ## 3. Provide proof of address To verify your place of residence, upload one of the following documents issued within the last **6 months**: * Utility bill (electricity, water, gas, or internet) * Official government letter or tax notice The document must clearly display your **full name**, **residential address**, and **issue date**. **Bank statements are not accepted** as proof of address. ## 4. Answer a few short questions You’ll be asked to complete a short questionnaire about your financial background — for example, your occupation and source of income. Your answer helps our compliance team understand the nature of your transactions and verify your Source of Funds. You may also attach or link to relevant materials (e.g., LinkedIn profile, portfolio, website). --- # Account Activation Source: https://docs.stablegate.com/docs/verification/compliance/individual/activation Account activation applies to **Standard Verification** only. Accounts with [Instant Verification](/docs/verification/compliance/individual) do not need activation. Once your Standard Verification is approved, your account will be marked as **Verified (Pending Activation)**. To fully activate it, complete **one of the following steps**: ## Option 1 — Test Deposit Make a **small test transfer (i.e. €1)** from your **personal bank account** (registered in your name) located in one of the **approved countries**: Australia • Austria • Belgium • Brazil • Canada • France • Germany • Greece • Hong Kong (China) • India • Indonesia • Ireland • Israel • Italy • Japan • Korea (South) • Luxembourg • Malaysia • Netherlands • New Zealand • Norway • Portugal • Saudi Arabia • Singapore • South Africa • Spain • Sweden • Switzerland • Turkey • United Kingdom • USA This confirms that your bank account belongs to you and is held with a regulated financial institution. **Note:** The test deposit is required **only once** for account activation. After your account is verified and activated, you can use **any of your own bank accounts** from **any country or financial institution.** ## Option 2 — Video Interview If you cannot make a test deposit from one of the approved countries, you can complete a **short video interview** with our compliance team instead. ### The interview * Takes **no longer than 5 minutes** * Is conducted **in English** * Covers basic **personal identification details** submitted during onboarding Please have your **original ID document** (the same one used during verification) ready for the session. **To schedule it, please contact:** 📩 [support@stablegate.com](mailto:support@stablegate.com) --- # Source of Funds Source: https://docs.stablegate.com/docs/verification/compliance/individual/sof If you wish to increase your monthly or annual transaction limits, you’ll need to verify your Source of Funds (SoF) — confirming where your funds come from. The standard lifetime transaction limit for verified individual accounts is €15,000. If you plan to transact above this threshold, you’ll be required to complete Source of Funds verification to unlock higher limits. Verification of Source of Funds typically takes **1 business day**. ## Examples of supporting documents 1. **Tax declaration** for the latest fiscal period; 2. **Bank statements** showing income or asset sale proceeds; 3. **Sale agreements** (real estate, vehicles, assets) and corresponding payment confirmations; 4. **Investment account statements** or brokerage reports; 5. **Notarized documents** confirming inheritance or gift; 6. For clients engaged in **crypto arbitrage**: * a detailed **description of the arbitrage model and platforms used** (e.g., exchanges involved, transfer routes, liquidity sources); * **transaction evidence** such as wallet statements or exchange account reports; * **proof of the original capital** used to start the activity (e.g., income from employment, investment, or asset sale). ## Examples of Source of Funds Explanations Below are several examples of clear and acceptable descriptions: ### Example 1 – Employment Income “I have been working as an accountant at ABC Limited since 2020. As proof of my source of funds, I can provide my employment contract, recent tax returns, and a link to my LinkedIn profile (link) for verification of my professional background.” ### Example 2 – Self-Employed / Freelance Work “I am a self-employed web designer with clients worldwide who pay for my services in cryptocurrency. I use Stablegate to convert my earnings into fiat currency. You can review my portfolio and client references on my website (link).” ### ✅ Tips for individual users * Be specific about what you do and how you earn. * If you receive payments in crypto, explain what services you provide and who your clients are (general description is enough). * Include links to online profiles, websites, or portfolios if available. * Avoid generic terms like “freelancer” or “investor” without further detail. --- # Business Verification (KYB) Source: https://docs.stablegate.com/docs/verification/compliance/business ## Purpose At **Stablegate**, corporate verification (KYB — *Know Your Business*) ensures that every registered company is legitimate, transparent, and compliant with **Swiss** requirements. This process protects both your business and the Stablegate ecosystem by preventing fraud, financial crime, and unauthorized activity. After registering your company and confirming your **business email address**, your account is automatically activated with a **monthly transaction limit of up to €1,000**. At this level, your company can transact only **with bank accounts registered in the same company name**. To unlock full access to Stablegate Business features and increase your limits, your company must complete **Verification (KYB)** — a simple and structured process. ## Who Completes the Onboarding Only **one authorized representative** — typically a **director** or **appointed company officer** — completes the **individual KYC onboarding** as part of the corporate verification. This includes the **selfie (liveness check)** and **account activation** via a **test deposit** or **video interview**. Information about other company participants, such as directors, shareholders, and **Ultimate Beneficial Owners (UBOs)**, is collected from **official company documents** — they do **not** complete individual onboarding. --- # Verification Process Source: https://docs.stablegate.com/docs/verification/compliance/business/verification The process usually takes **under 10 minutes**, and approval is typically completed within **one business day** after all documents are submitted. ## 1. Identify and verify the authorized representative The company’s **authorized representative** (usually the director or legal signatory) must complete **individual identification** to confirm their identity and authority to act on behalf of the company. Please provide: * **Government-issued ID with MRZ code** (passport, national ID or residence permit) * The **MRZ (Machine-Readable Zone)** is the section of two or three lines of letters and numbers at the bottom of your document, used for secure automated verification. * **Selfie (liveness check)** * **Proof of address** — utility bill or official government letter issued within the last 6 months ## 2. Upload company registration documents To confirm your company’s legal structure and ownership, please upload: * **Certificate of Incorporation** * **Company Register Extract** (recent, showing active legal status) * **Memorandum & Articles of Association** * **Confirmation of active status** (if not included in the register extract) * **Certificate of Directors** (listing current board members) * **Shareholder & ownership structure** (including all UBOs and ownership percentages) This allows our compliance team to confirm incorporation details, control structure, and authorized persons without requiring additional submissions. All files must be **official, complete, and clearly legible**, showing your **full company name**, **registration number**, and **jurisdiction**. ## 3. Complete the KYB questionnaire You’ll then complete a short online form providing key business details: * Nature and purpose of business activity * Industry and operational scope * Expected transaction volume and frequency * Ownership and control structure (based on corporate documents) * Source of funds * Licensing information (if applicable) * if your company operates as a **financial institution (FI)**, please provide your **regulatory license** or **registration number** issued by your supervisory authority (e.g., FINMA, FCA, MiCA or equivalent). Providing full and accurate information ensures faster and smoother processing. When completing your **KYB questionnaire**, please describe your company’s core business activity, source of funds, and how you intend to use Stablegate. Include references or public links (e.g., your company website, registry listing, or license). --- # Account Activation Source: https://docs.stablegate.com/docs/verification/compliance/business/activation After your company’s verification is approved, the **authorized representative** must complete one of the following steps to fully activate the account. ## Option 1 — Test Deposit Make a **small test transfer (i.e. €1)** from your **company’s official bank account** (registered under your company’s legal name) located in one of the **approved countries**: Australia • Austria • Belgium • Brazil • Canada • France • Germany • Greece • Hong Kong (China) • India • Indonesia • Ireland • Israel • Italy • Japan • Korea (South) • Luxembourg • Malaysia • Netherlands • New Zealand • Norway • Portugal • Saudi Arabia • Singapore • South Africa • Spain • Sweden • Switzerland • Turkey • United Kingdom • USA This confirms that your business account is valid and held with a regulated financial institution. **Note:** The test deposit is required **only once** for account activation. After verification, you may use **any of your company’s bank accounts** from **any country or financial institution**. ## Option 2 — Video Interview If the company cannot make a test deposit from one of the approved countries, the **authorized representative** can schedule a **short video interview** with our compliance team instead. ### The interview * Takes **no longer than 5 minutes** * Is conducted **in English** * Covers basic **personal identification details** submitted during onboarding Please have your **original ID document** (the same one used during verification) ready for the session. **To arrange it, please contact:** 📩 [support@stablegate.com](mailto:support@stablegate.com) --- # Source of Funds Source: https://docs.stablegate.com/docs/verification/compliance/business/sof The **standard lifetime transaction limit** for verified corporate accounts is **€15,000**. If your company plans to process transactions above this threshold, you’ll need to verify your **Source of Funds (SoF)** to unlock higher limits. This ensures that all funds used on the platform come from legitimate and transparent business activity. Verification typically takes **1 business day**. ## Examples of supporting documents 1. **Financial statements** (balance sheet, P&L) for the most recent period; 2. **Bank statements** confirming incoming funds; 3. **Contracts and invoices** with clients or partners; 4. **Documents confirming investments or loans** (agreements, shareholder resolutions, etc.); 5. For crypto-related entities: * a description of the **business model and operational structure**; * **exchange or wallet statements** showing relevant transactions; * documentation confirming the **initial capital** or source of business funds. ## Additional requirements for financial institutions If the corporate client is a **financial institution** (e.g., exchange, broker, asset manager, or payment company) and intends to use **client funds** on the platform, the following must be additionally provided: * a valid **regulatory license or registration certificate** issued by a competent authority (e.g., FINMA or equivalent foreign regulator); * the institution’s **AML/CFT policy and internal control framework**; * a description of the **client asset segregation and safeguarding procedure**; * confirmation that the funds used belong to **verified end clients** and are handled in compliance with applicable AML/CFT obligations. ## Examples of Source of Funds Descriptions Below are several examples of appropriate responses for different types of businesses: ### Example 1 – Financial Institution / Crypto Exchange “We are a licensed financial institution operating as a crypto exchange. We will be using client funds to conduct exchange and settlement operations through Stablegate. Our company website (link) and regulatory license (link) are publicly available in the official registry.” ### Example 2 – Marketing / Commercial Business “We are a marketing company specializing in brand promotion on social media with an annual revenue of approximately €100,000. As proof, we can provide our latest financial statement, and you can learn more about our services on our website (link).” ### Example 3 – Technology / SaaS Provider “We are a software development company providing B2B SaaS tools for e-commerce automation. Our clients pay us monthly subscriptions in both fiat and crypto. We will use Stablegate to process settlements and manage treasury operations. Company details and client references are available on our website (link).” ### Example 4 – Trading / Arbitrage / OTC Operations “We are a registered trading company engaged in digital asset arbitrage and OTC transactions across regulated exchanges. All funds originate from our own corporate capital and verified counterparties. We maintain full traceability of wallets and trading records and can provide exchange statements and internal audit reports upon request.” ### ✅ Tips for corporate clients * Clearly describe your **business model** and **fund flow**. * Mention your **license or registration** if you operate in a regulated sector. * Provide links to your **company website**, **official registry entry**, or **license listing**. * If your activity involves **trading, arbitrage, or settlements**, outline the compliance structure and counterparties. --- # Overview Source: https://docs.stablegate.com/docs/apiref Every endpoint of the Stablegate API v1 has its own page in the sidebar, with parameters, request body, responses and code samples. ## Base URL All requests go to `https://api.stablegate.com`. ## Authorization Send your API key in the `X-Api-Key` header on every request. Generate the key in the Developers section of the Stablegate app. ## OpenAPI specification Download the [OpenAPI specification](/openapi.json) to generate a client or import the API into your tooling. --- # Authorization Source: https://docs.stablegate.com/docs/apiref/auth Every request to the Stablegate API carries your API key in the `X-Api-Key` header. Requests go to `https://api.stablegate.com`. Generate the key in the Developers (https://app.stablegate.com/developers) section of the Stablegate app. The partner API does not require request signing. ```bash curl https://api.stablegate.com/v1/bank-providers/ \ --header 'X-Api-Key: YOUR_API_KEY' ``` ## Error responses A missing or invalid key is answered with `401`; a key without access to the requested resource, or without the Partner API enabled for the account, with `403`. Both use the common error envelope: | Status | `error_code` | |--------|--------------| | `401` | `devapi_unauthorized_error` | | `403` | `devapi_forbidden_error` | ```json { "trace_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "error_code": "devapi_unauthorized_error", "message": "", "detail": "" } ``` `message` explains the reason in plain text and its wording can change, so branch on `error_code`. The `detail` key is still returned for now and will be removed in a later release. See [Authentication](/docs/guide/getting-started/authentication) in the guide for creating keys, key rotation and code examples. --- # Changelog Source: https://docs.stablegate.com/docs/changelog This page lists changes to the Stablegate public documentation and API. Entries are ordered newest first. ## November 11, 2026 This release makes the API explain itself: every failed call answers in one envelope with a request id a partner can quote, the documentation covers that surface end to end, and a customer's verification state is readable without guessing from `kyc_level`. - `GET /v1/customers/{customer_uuid}/verification/` returns the latest KYC application with its status and the message compliance wrote for the customer, and the verification link can be re-issued while the application waits for the customer or for compliance. - The orders list reports the commission the platform actually charged, together with `commission_asset_code`, so the amount matches the [order details](/docs/guide/orders#get-order-details) for both on-ramps and off-ramps. - [`check-fees`](/docs/guide/fees-and-limits/fees#check-fees-api) names the asset the commission is charged in: on an off-ramp the amount is deducted in the crypto you send, not in EUR. - The [verification link](/docs/guide/customers#create-a-verification-link) section says which open applications accept a new link: `new`, `waiting` and `pending` keep it short-lived, so the partner asks for a fresh one. - The guide examples and the order list fields show the commission in the asset it is charged in, with `commission_asset_code` beside the amount. - Every response carries an `X-Request-Id` header: send your own value and the same id appears in the header and in the `trace_id` of an error, so a gateway timeout without a body still leaves a trace. - A new [Errors](/docs/guide/getting-started/errors) page covers the body of every error, the status codes, the `devapi_*` codes you can branch on, and when to retry. - A new [Quickstart](/docs/guide/getting-started/quickstart) page walks seven requests from an API key to a paid-out on-ramp order. - The [On-Ramp](/docs/guide/integration-guides/onramp) and [Off-Ramp](/docs/guide/integration-guides/offramp) guides describe `check-limits` the way the API returns it — only the rules the operation breaks — and name the fields a payout needs. - [External Accounts](/docs/guide/external-accounts) carry the third-party flag you set yourself when registering a bank account; payouts above the invoice threshold ask for the supporting document based on it. - The [Postman Collection](/docs/guide/getting-started/postman-collection) page and the API reference come from the same OpenAPI snapshot, so every endpoint is listed once. ## November 2, 2026 A customer's account can now be activated through the API: [Create a Verification Order](/docs/guide/orders#activate-an-account-with-a-verification-order) returns the bank details the customer pays the test deposit to, and `permissions.can_trade` turns `true` once the deposit arrives. A regular order does not activate an account. - A customer created by mistake can be closed through the API: [Close a Customer](/docs/guide/customers#close-a-customer) answers `204` and leaves the record visible in the `closed` state. ## October 31, 2026 A bank account now describes the payment scheme it moves money on. `POST` and `PUT /v1/customers/{customer_uuid}/bank-accounts/` take `supported_rails` (`sepa`, `swift`, `ach`, `wire`), `account_number` for accounts outside SEPA, `bank_routing_number` for US payments, and the address of the bank and of the account holder. See [External Accounts](/docs/guide/external-accounts#create-a-bank-account). - One record covers one scheme: SEPA takes a separate account from ACH and FedWire. - `holder_name` is the only field the request body requires. SEPA needs an `iban`, SWIFT needs a `bank_code` (BIC), US payments need `bank_country: "US"` and a 9-digit `bank_routing_number`. - The bank decides which details it needs to execute a payout. It can require the holder address and the bank address for SWIFT and US payouts even though the API accepts an account without them, and rejects the payout when the order is executed. ## October 30, 2026 - Orders that pay out to a third-party bank account wait for an invoice when the amount is above 15 000 EUR equivalent: such an order is created as usual but Stablegate does not execute it until the invoice is attached. See [Orders](/docs/guide/orders#third-party-payouts-and-the-invoice). - Stablegate no longer emails deposit instructions for orders you create through the API, so show the payment details to the customer on your own screen. Emails about the received deposit and the sent payout are unchanged. See [Integration Checklist](/docs/guide/integration-guides/checklist#what-you-are-building). ## October 29, 2026 The documentation has a new design: the [API Reference](/docs/apiref) has a page for every endpoint, and `/openapi.json`, `llms.txt` and the Postman collection are generated from the same specification. Existing links keep working. **Check these before you update.** - Limit amounts are decimal strings like `"5000.00"` — in `GET /limits/`, `POST /check-limits/` and the order list. Parse them as decimals, not numbers. See [Limits](/docs/guide/fees-and-limits/limits). - Every error, `422` included, returns `trace_id`, `error_code`, `message` and `details`. The `detail` key is still there and goes away in a later release, so read `message` and `error_code`. - A customer that does not exist or belongs to another partner answers `404` instead of `403`. **New in orders.** - `source.deposit_status` shows that the customer's transfer has arrived while the order is still `new`. See [Orders](/docs/guide/orders#deposit-status). - `exchange_rate` and `rate_valid_until` show the rate the order holds and until when. - `source.deposit_bank_info` carries `holder_name` and `bank_address` to give to the customer. - Limit rules carry `rail_types`, `rail_codes` and `operation_types`. **Also fixed.** - `POST /v1/customers/{customer_uuid}/orders/{order_uuid}/cancel/` works for orders in `new`. - Creating a customer with an email that is already taken answers `400` with `devapi_email_occupied_error` instead of `500`. - An `Idempotency-Key` cache entry belongs to the API key that made the request. - A wallet address must be unique across all customers on its network; a conflict answers `oa_client_external_account_address_exists`. - A path without the trailing slash redirects to `https://` and keeps `X-Api-Key`. ## October 2, 2026 This release makes bank accounts easier to register. `bank_code` is no longer required: Stablegate derives the BIC and the bank name from the IBAN, and the new `GET /v1/bank-accounts/info-by-iban/` shows what will be filled in — see [External Accounts](/docs/guide/external-accounts#create-a-bank-account). Order details now also carry the hash of the customer's crypto deposit in `source.transaction_hash`, so you can show the customer that the transfer has been seen. Also in this release: - When the bank registry has no BIC for an IBAN, creating a bank account answers `400` with `devapi_bank_code_required_error`; pass `bank_code` yourself in that case. - `exchange_rate` returned by `check-fees` now includes the exchange spread: it is the rate `destination.amount` is calculated with. Amounts are rounded to the precision of their asset instead of two decimals. See [Fees](/docs/guide/fees-and-limits/fees). - An order whose payout is rejected by the payment provider now moves from `processing` to `rejected`; it used to end up in `failed`. A `failed` order can now be stopped by Stablegate operations and move to `stopped`. See [Orders](/docs/guide/orders#order-lifecycle). - A webhook now receives only the events of the account it belongs to. - An API key whose account has no access to the Partner API answers `403` instead of an internal error. When Stablegate cannot verify a key because an internal service is unavailable, the API answers `503` — retry the request. See [Authentication](/docs/guide/getting-started/authentication#error-responses). ## September 15, 2026 This release brings **Instant (Light) Verification** to the Partner API. `POST /v1/customers/{customer_uuid}/verification/instant/` takes the customer's first name, last name, and date of birth, runs the automated AML screening, and approves the application when the screening is clear. It is documented on the [Customers](/docs/guide/customers#start-instant-verification) guide together with the existing Standard verification link endpoint. Also in this release: - `GET /v1/customers/{customer_uuid}/` now returns `kyc_level`, so you can poll the outcome of a verification on the detail endpoint (previously only the list endpoint had it). - Documented what **Instant (Light) Verification** consists of on [Individual Verification](/docs/verification/compliance/individual): first name, last name, and date of birth, followed by an automated AML screening; approval is automatic when the screening is clear. Verification has to be started by the customer or by the partner through the API — it does not begin on its own after registration. - Clarified on [Account Activation](/docs/verification/compliance/individual/activation) that the test deposit or video interview applies to Standard verification only; Instant verification needs no activation. The `permissions.can_trade` note on [Customers](/docs/guide/customers) now says which level requires activation. ## August 25, 2026 This release corrects the documentation where it disagreed with the API. Two corrections matter most for an integration. A customer cannot transact before verification: the €1,000 monthly limit applies only after **Light Verification**, and before any verification the limit is €0 — see [Individual Verification](/docs/verification/compliance/individual) and [Limits](/docs/guide/fees-and-limits/limits). And every transaction is charged whichever is greater, the percentage rate or the **€20 minimum commission**, which was previously undocumented and could look like a flat fee at small transaction sizes — see [Fees](/docs/guide/fees-and-limits/fees#minimum-commission). Also in this release: - Corrected the [Customers](/docs/guide/customers) guide: customer records cannot be updated after creation (only `GET` is supported on `/v1/customers/{customer_uuid}/`; `PATCH`/`PUT` return `405`). The March 4, 2026 entry below incorrectly listed "update" among the supported operations. - Documented `bank_code` (BIC/SWIFT) as a **required** field on [bank account creation](/docs/guide/external-accounts#create-a-bank-account) — it was previously listed as optional. - Documented the `status` and `verified_at` fields on crypto wallet responses under [External Accounts](/docs/guide/external-accounts#crypto-wallets), so you can confirm Satoshi Test / WalletConnect ownership verification via the API before placing an order. - Clarified in [Customers](/docs/guide/customers) that `email`/`username` uniqueness is enforced across your whole legal entity, not per customer `type`. ## April 21, 2026 - Published machine-readable resources for LLM coding agents at the site root: [`llms.txt`](/llms.txt) (curated index following the [llmstxt.org](https://llmstxt.org/) spec), [`llms-full.txt`](/llms-full.txt) (full documentation bundle in one file), and [`openapi.json`](/openapi.json) (raw OpenAPI 3.1 spec). - Added the [For LLM Agents](/docs/guide/getting-started/llm-agents) page describing these resources and when to use each. ## April 20, 2026 - Updated the list of [non-serviced countries](/docs/verification/common/countries) to reflect current sanctions and policy. ## April 13, 2026 - Added the [Bank Providers](/docs/guide/bank-providers) guide and API endpoint for discovering banks, their supported rails, and the exchange routes accessible through each bank. - Added the `exchange_rate` field to fee quotes so you can surface the live rate before placing an order. See [Fees](/docs/guide/fees-and-limits/fees). - Cross-linked the Bank Providers concept from the [Introduction](/docs/guide), [Getting Started](/docs/guide/getting-started), and the integration guides. ## April 10, 2026 - Added the [Webhooks](/docs/guide/webhooks) guide: event types, payload schemas, HMAC signature verification, and endpoint registration. - Updated [Orders](/docs/guide/orders) and [Getting Started](/docs/guide/getting-started) to reference webhook-driven order status updates as an alternative to polling. ## March 20, 2026 - Documented the `external_reference` field on orders, which lets you correlate Stablegate orders with identifiers in your own system. See [Orders](/docs/guide/orders). - Clarified the end-to-end integration flow in the [Introduction](/docs/guide) and [Getting Started](/docs/guide/getting-started) pages. - Corrected inaccuracies across the [Customers](/docs/guide/customers), [Idempotency](/docs/guide/getting-started/idempotency), [Fees](/docs/guide/fees-and-limits/fees), [Limits](/docs/guide/fees-and-limits/limits), and integration guides. - Refreshed the [Postman collection](/docs/guide/getting-started/postman-collection) to match the current API contract. ## March 4, 2026 - Added the [Customers](/docs/guide/customers) guide covering creation, retrieval, listing, and update of customer records, along with customer UUID scoping rules. - Added the [Idempotency](/docs/guide/getting-started/idempotency) guide covering the `Idempotency-Key` header and safe-retry semantics. - Expanded the OpenAPI specification to cover the full set of `v1` endpoints and schemas rendered on the [API Reference](/docs/apiref). ## February 16, 2026 - Revised the [Fees](/docs/guide/fees-and-limits/fees) and [Limits](/docs/guide/fees-and-limits/limits) guides with updated calculation examples and field descriptions. ## February 13, 2026 - Replaced the initial placeholder guides with the current resource-oriented documentation. Integration content now lives under [Getting Started](/docs/guide/getting-started), [Integration Guides](/docs/guide/integration-guides), and the per-resource guides (Customers, External Accounts, Exchange Routes, Orders, Fees & Limits). - Updated the OpenAPI specification accordingly. ## December 5, 2025 - Added the [Terms of Business](/docs/verification/common/terms) page to the Verification section. ## October 30, 2025 - Initial release of the public Stablegate documentation, including integration guides, API reference, and verification rules. --- # OpenAPI Endpoint Summary Spec: OpenAPI 3.1.0. Title: Stablegate API. Version: 1.0.0. Full machine-readable spec: https://docs.stablegate.com/openapi.json API reference: https://docs.stablegate.com/docs/apiref ## Bank accounts ### GET /v1/customers/{customer_uuid}/bank-accounts/ Get Bank Accounts Lists the bank accounts registered for the customer. operationId: `get_v1_customers_customer_uuid_bank_accounts` Parameters: - `customer_uuid` (path) (required): Customer UUID Responses: - `200`: Successful Response - `422`: Validation Error ### POST /v1/customers/{customer_uuid}/bank-accounts/ Create Bank Account Registers a bank account for the customer, deriving the bank code and name from the IBAN when they are omitted. devapi_bank_code_required_error means the registry has no BIC for this IBAN — pass bank_code explicitly. operationId: `post_v1_customers_customer_uuid_bank_accounts` Parameters: - `customer_uuid` (path) (required): Customer UUID Request body: application/json (required) Responses: - `200`: Successful Response - `422`: Validation Error ### GET /v1/customers/{customer_uuid}/bank-accounts/{account_uuid}/ Get Bank Account Returns one of the customer's registered bank accounts. operationId: `get_v1_customers_customer_uuid_bank_accounts_account_uuid` Parameters: - `account_uuid` (path) (required): Bank account UUID - `customer_uuid` (path) (required): Customer UUID Responses: - `200`: Successful Response - `422`: Validation Error ### PUT /v1/customers/{customer_uuid}/bank-accounts/{account_uuid}/ Update Bank Account Updates a registered bank account, deriving the bank code and name from the IBAN again when they are omitted. No error is raised when the BIC can't be resolved. operationId: `put_v1_customers_customer_uuid_bank_accounts_account_uuid` Parameters: - `account_uuid` (path) (required): Bank account UUID - `customer_uuid` (path) (required): Customer UUID Request body: application/json (required) Responses: - `200`: Successful Response - `422`: Validation Error ### DELETE /v1/customers/{customer_uuid}/bank-accounts/{account_uuid}/ Delete Bank Account Deletes a registered bank account from the customer. operationId: `delete_v1_customers_customer_uuid_bank_accounts_account_uuid` Parameters: - `account_uuid` (path) (required): Bank account UUID - `customer_uuid` (path) (required): Customer UUID Responses: - `204`: Successful Response - `422`: Validation Error ### GET /v1/bank-accounts/info-by-iban/ Get Bank Info By Iban Looks up the BIC, bank name and country behind an IBAN before you register the account. bank_code and bank_name can be null when the bank isn't in the registry, which is common for EMIs and fintechs. operationId: `get_v1_bank_accounts_info_by_iban` Parameters: - `iban` (query) (required): IBAN to look up, spaces allowed Responses: - `200`: Successful Response - `422`: Validation Error ## Bank Providers ### GET /v1/bank-providers/ Get Bank Providers Lists the bank providers available for fiat operations with their rails and exchange routes. operationId: `get_v1_bank_providers` Parameters: - `rail` (query): Filter by rail (sepa, swift, ach) - `country` (query): Filter by bank country (ISO alpha-3, e.g. CHE, LTU) Responses: - `200`: Successful Response - `422`: Validation Error ## Customer Wallets ### GET /v1/customers/{customer_uuid}/wallets/satoshitest/ Get Account For Satoshitest Returns the operational wallet address the customer sends a micro-transaction to as proof of wallet ownership. operationId: `get_v1_customers_customer_uuid_wallets_satoshitest` Parameters: - `customer_uuid` (path) (required): Customer UUID - `rail` (query) (required): Crypto rail code Responses: - `200`: Successful Response - `422`: Validation Error ### GET /v1/customers/{customer_uuid}/wallets/ Get Customer Wallets List Lists the crypto wallets registered for the customer. operationId: `get_v1_customers_customer_uuid_wallets` Parameters: - `customer_uuid` (path) (required): Customer UUID Responses: - `200`: Successful Response - `422`: Validation Error ### POST /v1/customers/{customer_uuid}/wallets/ Create Customer Wallet Registers a crypto wallet for the customer, with an ownership proof on Ethereum and Polygon. operationId: `post_v1_customers_customer_uuid_wallets` Parameters: - `customer_uuid` (path) (required): Customer UUID Request body: application/json (required) Responses: - `200`: Successful Response - `422`: Validation Error ### GET /v1/customers/{customer_uuid}/wallets/{wallet_uuid}/ Get Customer Wallet Returns one of the customer's registered crypto wallets. operationId: `get_v1_customers_customer_uuid_wallets_wallet_uuid` Parameters: - `wallet_uuid` (path) (required): Customer wallet UUID - `customer_uuid` (path) (required): Customer UUID Responses: - `200`: Successful Response - `422`: Validation Error ### PUT /v1/customers/{customer_uuid}/wallets/{wallet_uuid}/ Update Customer Wallet Updates a registered crypto wallet; a new address needs its ownership proof again. operationId: `put_v1_customers_customer_uuid_wallets_wallet_uuid` Parameters: - `wallet_uuid` (path) (required): Customer wallet UUID - `customer_uuid` (path) (required): Customer UUID Request body: application/json (required) Responses: - `200`: Successful Response - `422`: Validation Error ### DELETE /v1/customers/{customer_uuid}/wallets/{wallet_uuid}/ Delete Customer Wallet Deletes a registered crypto wallet from the customer. operationId: `delete_v1_customers_customer_uuid_wallets_wallet_uuid` Parameters: - `wallet_uuid` (path) (required): Customer wallet UUID - `customer_uuid` (path) (required): Customer UUID Responses: - `204`: Successful Response - `422`: Validation Error ## Customers ### POST /v1/customers/ Create Customer Registers an individual or corporate customer and returns the UUID that scopes every later operation. operationId: `post_v1_customers` Request body: application/json (required) Responses: - `200`: Successful Response - `422`: Validation Error ### GET /v1/customers/ Get Customers List Retrieves a paginated list of your customers with optional filtering and sorting. operationId: `get_v1_customers` Parameters: - `limit` (query): - `offset` (query): - `client_id` (query): - `email` (query): - `phone` (query): - `username` (query): - `full_name` (query): - `type` (query): - `country_code` (query): - `email_verified` (query): - `kyc_level_key` (query): - `risk_level_key` (query): - `pattern` (query): - `status` (query): - `partner_uuid` (query): - `partner_email` (query): - `user_uuid` (query): - `user_email` (query): - `external_reference` (query): - `email__like` (query): - `phone__like` (query): - `username__like` (query): - `full_name__like` (query): - `client_id__like` (query): - `partner_full_name__like` (query): - `partner_email__like` (query): - `user_first_name__like` (query): - `user_last_name__like` (query): - `user_email__like` (query): - `external_reference__like` (query): - `created_at__lt` (query): - `created_at__lte` (query): - `created_at__gt` (query): - `created_at__gte` (query): - `with_attributes` (query): - `is_funded` (query): - `is_funded_updated_at__lt` (query): - `is_funded_updated_at__lte` (query): - `is_funded_updated_at__gt` (query): - `is_funded_updated_at__gte` (query): - `accuity_sanctions` (query): - `accuity_sanctions_updated_at__lt` (query): - `accuity_sanctions_updated_at__lte` (query): - `accuity_sanctions_updated_at__gt` (query): - `accuity_sanctions_updated_at__gte` (query): - `video_verification_passed` (query): - `video_verification_passed_updated_at__lt` (query): - `video_verification_passed_updated_at__lte` (query): - `video_verification_passed_updated_at__gt` (query): - `video_verification_passed_updated_at__gte` (query): - `first_deposit_date__lt` (query): - `first_deposit_date__lte` (query): - `first_deposit_date__gt` (query): - `first_deposit_date__gte` (query): - `sort` (query): - `order_by` (query): Responses: - `200`: Successful Response - `422`: Validation Error ### GET /v1/customers/{customer_uuid}/ Get Customer Returns the full customer profile with its state, KYC level and the permissions that allow trading. operationId: `get_v1_customers_customer_uuid` Parameters: - `customer_uuid` (path) (required): Customer UUID Responses: - `200`: Successful Response - `422`: Validation Error ### DELETE /v1/customers/{customer_uuid}/ Close Customer Closes a customer created by mistake; allowed only while the customer is new and no verification has been submitted, and cannot be undone. The record stays visible with `state: closed`, and its email stays taken. operationId: `delete_v1_customers_customer_uuid` Parameters: - `customer_uuid` (path) (required): Customer UUID Responses: - `204`: Successful Response - `422`: Validation Error ### GET /v1/customers/{customer_uuid}/account-opening-letter/ Download Account Opening Letter Downloads the account opening letter as a PDF, available while permissions.can_download_aol is true. operationId: `get_v1_customers_customer_uuid_account_opening_letter` Parameters: - `customer_uuid` (path) (required): Customer UUID Responses: - `200`: Successful Response - `422`: Validation Error ## Exchange Routes ### GET /v1/exchange-routes/ Get All Supported Exchange Routes Lists every supported exchange route with its source and destination rail and asset. operationId: `get_v1_exchange_routes` Responses: - `200`: Successful Response ### POST /v1/exchange-routes/check/ Check Exchange Route Tells whether a route is operational right now, before the customer sends funds. operationId: `post_v1_exchange_routes_check` Request body: application/json (required) Responses: - `200`: Successful Response - `422`: Validation Error ## Fees ### POST /v1/customers/{customer_uuid}/check-fees/ Check Customer Fees Calculates the commission and exchange rate for a route before the customer places an order. operationId: `post_v1_customers_customer_uuid_check_fees` Parameters: - `customer_uuid` (path) (required): Customer UUID Request body: application/json (required) Responses: - `200`: Successful Response - `422`: Validation Error ## Limits ### GET /v1/customers/{customer_uuid}/limits/ Customer Limits Lists the customer's active limits with the amounts used and still available. operationId: `get_v1_customers_customer_uuid_limits` Parameters: - `customer_uuid` (path) (required): Responses: - `200`: Successful Response - `422`: Validation Error ### POST /v1/customers/{customer_uuid}/check-limits/ Check Customer Limits Checks an amount against the customer's limits for a route, so the order is not rejected later. operationId: `post_v1_customers_customer_uuid_check_limits` Parameters: - `customer_uuid` (path) (required): Request body: application/json (required) Responses: - `200`: Successful Response - `422`: Validation Error ## Orders ### POST /v1/customers/{customer_uuid}/orders/ Create Order Creates an exchange order that sends funds from a bank account or wallet to the destination account. operationId: `post_v1_customers_customer_uuid_orders` Parameters: - `customer_uuid` (path) (required): Request body: application/json (required) Responses: - `200`: Successful Response - `422`: Validation Error ### GET /v1/customers/{customer_uuid}/orders/ Get Orders Lists the customer's orders with filters by state, rail, asset and date. operationId: `get_v1_customers_customer_uuid_orders` Parameters: - `customer_uuid` (path) (required): - `limit` (query): - `offset` (query): - `uuid` (query): - `external_id__like` (query): - `holding_uuid` (query): - `account_uuid` (query): - `is_approved` (query): - `is_third_party_payment` (query): - `is_approve_deposited` (query): - `incoming_rails_code` (query): - `incoming_rails_type` (query): - `incoming_provider_name` (query): - `incoming_asset_code` (query): - `incoming_custodian_account__like` (query): - `incoming_client_account__like` (query): - `outgoing_rails_code` (query): - `outgoing_rails_type` (query): - `outgoing_provider_name` (query): - `outgoing_asset_code` (query): - `outgoing_custodian_account__like` (query): - `outgoing_client_account__like` (query): - `is_manual` (query): - `is_otc` (query): - `approved_by` (query): - `created_by` (query): - `approved_at__gte` (query): - `approved_at__lte` (query): - `created_at__gte` (query): - `created_at__lte` (query): - `updated_at__gte` (query): - `updated_at__lte` (query): - `sort` (query): - `order_by` (query): - `deleted__include` (query): - `account_code__like` (query): - `account_name__like` (query): - `account_type` (query): - `pipeline_type` (query): - `client_uuid` (query): - `status` (query): Responses: - `200`: Successful Response - `422`: Validation Error ### POST /v1/customers/{customer_uuid}/orders/verification/ Create Verification Order Creates a verification order whose test deposit activates a Standard-verified customer for trading. The customer pays a fixed verification amount (1 unit of the asset; it is kept as the order commission) from their own bank account to the bank details in `source.deposit_bank_info`, quoting `reference`; once the deposit arrives the order completes automatically and the account is activated. Only bank rails and fiat assets are accepted. When verification is not configured for the requested rail, the call answers 400. Poll the order with `GET /customers/{customer_uuid}/orders/{order_uuid}/`. operationId: `post_v1_customers_customer_uuid_orders_verification` Parameters: - `customer_uuid` (path) (required): Request body: application/json (required) Responses: - `200`: Successful Response - `422`: Validation Error ### GET /v1/customers/{customer_uuid}/orders/{order_uuid}/ Get Order Returns one order with its deposit, destination and commission details. operationId: `get_v1_customers_customer_uuid_orders_order_uuid` Parameters: - `order_uuid` (path) (required): Order UUID - `customer_uuid` (path) (required): Responses: - `200`: Successful Response - `422`: Validation Error ### POST /v1/customers/{customer_uuid}/orders/{order_uuid}/cancel/ Cancel Order Cancels an order that is still new; once processing starts the order cannot be cancelled. operationId: `post_v1_customers_customer_uuid_orders_order_uuid_cancel` Parameters: - `order_uuid` (path) (required): Order UUID - `customer_uuid` (path) (required): Responses: - `204`: Successful Response - `422`: Validation Error ### POST /v1/customers/{customer_uuid}/orders/{order_uuid}/third-party-document/ Attach Invoice Uploads the invoice or contract a third-party payout needs, and attaches it to the order. Needed when the order has `third_party_document_required: true` and no document yet; the order must still be `new`. One document per order. Without the document the order is not executed automatically and waits for the operator; once it is attached, the order is processed under the usual auto-processing rules. operationId: `post_v1_customers_customer_uuid_orders_order_uuid_third_party_document` Parameters: - `order_uuid` (path) (required): Order UUID - `customer_uuid` (path) (required): Request body: multipart/form-data (required) Responses: - `200`: Successful Response - `422`: Validation Error ## Verifications ### GET /v1/customers/{customer_uuid}/verification/ Get Customer Verification Returns the latest KYC application of the customer and the level the customer holds. The status shows whether compliance waits for the customer, for us, or has finished, and `message` carries what compliance asked the customer for. operationId: `get_v1_customers_customer_uuid_verification` Parameters: - `customer_uuid` (path) (required): Responses: - `200`: Successful Response - `422`: Validation Error ### POST /v1/customers/{customer_uuid}/verification/link/ Create Verification Link Starts Standard verification and returns the hosted link to send the customer to. operationId: `post_v1_customers_customer_uuid_verification_link` Parameters: - `customer_uuid` (path) (required): Responses: - `200`: Successful Response - `422`: Validation Error ### POST /v1/customers/{customer_uuid}/verification/import/ Import Verification Data Imports verification results from another KYC application by its share token. operationId: `post_v1_customers_customer_uuid_verification_import` Parameters: - `customer_uuid` (path) (required): Request body: application/json (required) Responses: - `204`: Successful Response - `422`: Validation Error ### POST /v1/customers/{customer_uuid}/verification/instant/ Start Instant Verification Starts Instant KYC verification for an individual customer and submits their identity details for screening. The customer must currently have no KYC level and no other active KYC application. AML screening runs after submission; poll GET /v1/customers/{customer_uuid}/ for the outcome: kyc_level is set once the application is approved. operationId: `post_v1_customers_customer_uuid_verification_instant` Parameters: - `customer_uuid` (path) (required): Request body: application/json (required) Responses: - `200`: Successful Response - `422`: Validation Error