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 (server-side, trusted) | Use @vortexfi/sdk. |
| Python (server-side, trusted) | Use vortex-sdk-python. |
| Browser, mobile, WebView | 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.
Do not call the raw ramp API from a browser. Browsers cannot safely hold sk_* keys or ephemeral secrets. Use the Widget or proxy through a trusted backend.
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.