This page is written so that an AI coding agent (or a human engineer using one) can build a production-quality Vortex integration in any language or stack. It also explains how to keep these docs themselves useful when retrieved into a coding agent's context.
When you point an AI coding agent at Vortex:
vortex-integration. If this repository is open in Codex, the skill is discovered automatically from $REPO_ROOT/.agents/skills/vortex-integration/SKILL.md; if you are integrating Vortex from another repository, install the public skill directory URL (https://github.com/pendulum-chain/vortex/tree/main/.agents/skills/vortex-integration) first, then match the user's task to the relevant recipe before implementing.@vortexfi/sdk you are mirroring. The SDK's behavior is the reference implementation; if your integration disagrees with it, the SDK wins.chainId, verifyingContract, to, value, data, and ramp/phase identifiers must match what your application requested for the current rampId.| Your runtime | Path |
|---|---|
| Node.js or modern browser with a custom UX | Use @vortexfi/sdk; browser code must authenticate through accessTokenProvider, never an sk_*. |
| Python (server-side, trusted) | Use vortex-sdk-python. |
| Browser, mobile, or WebView preferring a hosted UX and hosted custody | Use the Vortex Widget. |
| Anything else (Go, Rust, Elixir, Java, Ruby, PHP, .NET, Deno, edge runtimes, …) | Reimplement the SDK behavior against the raw API as described in Section D below. |
Every path supports all live fiat corridors: BRL (PIX), EUR (SEPA), USD (ACH), MXN (SPEI), COP, and ARS (CBU). The corridor determines the register-time fields and the fiat settlement step, not the integration shape — see Fiat Corridors for per-corridor requirements.
Ramping requires an onboarded (KYC/KYB-approved) user. Onboarding is a separate, corridor-specific flow that most corridors also expose through the API — see Section H before assuming the app or Widget is required.
Do not expose an sk_* or reimplement signing against the raw ramp API in a browser. An approved origin means Vortex has added your exact browser origin to its allowlist; request it at support@vortexfinance.co before you integrate, because unapproved origins fail at the CORS preflight. Use the browser build of @vortexfi/sdk with Bearer authentication on an approved origin, or use the Widget. Browser SDK users explicitly accept that ephemeral secrets are generated in browser memory and backed up to plaintext same-origin localStorage by default.
vortex-sdk-python)vortex-sdk-python is a process-bridge wrapper around the native Node.js SDK. It spawns the Node SDK and exposes a Python-friendly surface, so the behavior, custody model, and supported flows match @vortexfi/sdk exactly.
pip install vortex-sdk-python
from vortex_sdk import VortexSdk, RampDirection, FiatToken, EvmToken, Networks
sdk = VortexSdk(
api_base_url="https://api.vortexfinance.co",
public_key="pk_live_...",
secret_key="sk_live_...",
store_ephemeral_keys=True,
)
quote = sdk.create_quote(
ramp_type=RampDirection.BUY,
from_="pix",
to=Networks.Polygon,
input_amount="150",
input_currency=FiatToken.BRL,
output_currency=EvmToken.USDC,
)
ramp = sdk.register_ramp(quote, destination_address="0x...", tax_id="12345678900")
print(ramp.deposit_qr_code)
sdk.start_ramp(ramp.id)
Operational notes specific to the Python wrapper:
ephemerals_{rampId}.json is written unencrypted in the working directory.VortexSdk(...) instance for the lifetime of your service.Refer to the PyPI page for the latest version, function names, and breaking-change notes: https://pypi.org/project/vortex-sdk-python.
If your stack is neither Node nor Python, build a thin client that mirrors what @vortexfi/sdk does. The contract has six parts; implement them in this order.
Your client needs:
apiBaseUrl — https://api.vortexfinance.co (prod) or https://api-sandbox.vortexfinance.co (sandbox).publicKey — pk_live_* / pk_test_*. Sent in request bodies as apiKey for attribution.secretKey — sk_live_* / sk_test_*. Sent as X-API-Key header. Server-side only.Reject startup if a sk_live_* key is detected in a browser-shaped runtime.
POST /v1/quotes
Request body: see Quotes And Pricing. Treat monetary fields as strings end-to-end; never parse them into floats. Store id, expiresAt, fee, and the resolved route. Surface expiry to the caller as a domain error.
POST /v1/ramp/register
X-API-Key: sk_*
Before calling register, generate the two ephemeral accounts the ramp uses. The API accepts exactly two account types, and one account of each covers every leg of the route:
EVM signing account).Send only the public addresses, in the signingAccounts array of the register body:
{
"quoteId": "QUOTE_ID",
"signingAccounts": [
{ "type": "Substrate", "address": "5..." },
{ "type": "EVM", "address": "0x..." }
],
"additionalData": { "destinationAddress": "0x..." }
}
type must be "Substrate" or "EVM" — these are the only recognized values, and any other type is ignored. There is no publicKey field on register — partner attribution rides on the quote's apiKey. Persist the secret keys to your secure store, keyed by the not-yet-issued ramp; once the response returns a rampId, rekey the store entry. Never log the secrets.
The response contains:
rampIdunsignedTxs — an ordered list of transactions to signEach unsigned transaction declares its network, signer address, transaction format (evm-transaction, evm-typed-data, or substrate-extrinsic), and the payload bytes or fields to sign.
For each unsigned transaction:
tx.signer equals an ephemeral address you control → sign with the matching ephemeral key.tx.signer equals the user's wallet address → return the payload to the user's wallet for signing (EIP-712 typed data, EVM transaction, or Substrate extrinsic). Never sign user-controlled transactions on the server.chainId matches the network the SDK config declared.to / verifyingContract is one of the Vortex-published contracts for that network.value, asset, and amount match the current ramp quote.NUMBER_OF_PRESIGNED_TXS = 5).maxPriorityFeePerGas and maxFeePerGas returned by the node by 3× before signing.POST /v1/ramp/update
X-API-Key: sk_*
Body includes the rampId, the transaction reference, and either the signed payload or the broadcast transaction hash. The exact shape is defined in the OpenAPI file; do not guess fields.
POST /v1/ramp/start
X-API-Key: sk_*
On a buy, where the fiat payment instructions appear depends on the corridor:
depositQrCode (PIX) is released once the presigned transactions submitted via update pass validation — on the update response and on GET /v1/ramp/{id}, not on the register response. Show it; wait for the user to pay; then call start. (The SDK performs the update inside registerRamp, so SDK callers see it on the returned ramp process.)ibanPaymentData (IBAN, receiver name, payment reference) follows the same release rule as depositQrCode. Show it; the user completes the SEPA transfer; then call start.achPaymentData contains the bank transfer instructions for the corridor's rail (ACH, SPEI, CBU). Display them verbatim; the ramp continues automatically once the deposit is confirmed.On a sell, the flow is the same in every corridor: the user signs the user-owned transaction(s), you submit them via update, then call start. Vortex pays out on the corridor's rail — the user's PIX key (BRL), SEPA account (EUR), or the saved bank account referenced by fiatAccountId (USD, MXN, COP, ARS).
POST /v1/webhook against quoteId or sessionId. Verify every delivery using RSA-PSS / SHA-256 against GET /v1/public-key. See Webhooks.GET /v1/ramp/{id} for live user-facing UI.GET /v1/ramp/{id}/errors for support.These are not optional. The SDK handles them for you; a custom client must implement them explicitly.
rampId. Keep them until the ramp is COMPLETE or FAILED and any recovery window has passed. Never transmit secrets to Vortex, support, logs, or analytics. See Ephemeral Key Custody.register, update, and start with idempotency keys at your layer. Retries must not produce duplicate ramps.register. Create a fresh quote and re-prompt the user.X-Vortex-Timestamp is outside an acceptable window (300s is a reasonable default).quoteId, rampId, sessionId, partner order ID, user identifier, webhook IDs, and a reference to the ephemeral-key backup. Without these you cannot support users or reconcile.BigDecimal, decimal.Decimal).pk_test_* / sk_test_* against api-sandbox.vortexfinance.co. Never mix test keys with the live base URL or vice versa.Mirror those gaps deliberately. If your integration adds behavior the SDK lacks (encryption at rest, backup rotation, idempotency keys, retries), document it for your operators.
Before going live without the SDK:
sk_* keys never reach a browser.POST /v1/ramp/update called with the exact transaction reference returned by register.rampId and ephemeral backup.See also Production Checklist.
Where a corridor supports it, onboarding runs through the API without any Vortex UI.
Two profile models can run these flows, and the choice is independent of how you build your UI:
Neither model changes the three-part contract below:
Discover the flow.
GET /v1/onboarding/requirements?country=BR&customerType=business
Public, no auth. The response is a static workflow index: required documents, and ordered steps of kind api (call the referenced operation), direct-upload (HTTP PUT of file bytes to a presigned URL returned by the preceding step), or hosted (open a provider URL). A 404 means the country/customer-type combination has no supported flow.
Execute the steps in order. For each api step, resolve requestSchema against the response's openapiUrl — that schema is the complete field contract; never invent fields. Merge in the step's fixedBody/fixedQuery verbatim, and fill each derivedValues entry from the named earlier step's response (e.g. "body.subAccountId": "step 1 response subAccountId"). Honor condition (skip if not met) and repeatFor (run once per document, UBO, or related person). All execution steps require your normal authentication (Section D.1); a managed-profile manager adds X-Managed-Profile-Id — see Authentication And API Keys.
Poll for the outcome. Discovery deliberately omits every GET: readiness checks, status polling, and per-corridor behavior (statuses, retry rules, error taxonomies) are documented in Fiat Corridors. Track completion through GET /v1/onboarding/status or the corridor's specific status operation. There is no synchronous approval.
Non-negotiable rules for an agent implementing these flows:
2xx response marks a user approved. Only the provider's decision, surfaced through the status endpoints, does.409 … requires reconciliation means stop and contact support — never vary the payload or idempotency key to get past it).standard method; a Sumsub token import commits it to sumsub_share_token and blocks the standard path. Decide the method before touching either flow.requirementsVersion alongside the docs commit and SDK version you already record (Section A), and re-run discovery when it changes.If your platform must create and control headless customers — no Vortex login or UI for the end customer, and no later claiming flow — use managed child profiles and run every onboarding and ramp operation on the child's behalf, either with the manager credential plus X-Managed-Profile-Id or with child-owned credentials. All discovery-published onboarding steps and the full ramp lifecycle accept this delegation, subject to the manager's corridor policy; webhooks do not (poll instead). The walkthrough with examples is Managed Profiles; the authoritative contract is in Authentication And API Keys. Agents implementing this pattern must key their idempotency and state on the manager-scoped externalSubjectId → profileId mapping, and must complete a BR child's Sumsub token import before any status read for that child (the method-lock rule above).