Production APIBase URL: https://vulta.one/api

Integrate payments.
Keep the funds.

Production contracts for hosted checkout, embedded checkout, headless card and crypto payments, and signed merchant webhooks.

Settlement model

Non-custodial by design.

Crypto settles directly to merchant-controlled wallets. Card providers may perform payer-side identity checks before converting fiat to crypto.

01 / Start here

Choose the integration that matches your checkout.

Create all merchant-owned resources from your server. Never expose a Vulta API key in browser, mobile, widget, or bot client code.

Hosted checkout

Max or Business

Create a one-time checkout URL, redirect the payer, and fulfill from the webhook.

Widget or iframe

Max or Business

Create a reusable payment link, then open it inside your site with the official widget or embed route.

Headless API

Business

Build your own UI and receive either a crypto address or card-provider URL.

Before your first request

Add at least one active payout destination.
Add an active Polygon USDC destination before enabling card payments.
Generate API keys from an authenticated dashboard session.
Configure an HTTPS webhook and a signing secret before taking orders.

Authentication

Payment links, payout destinations, payment requests, hosted sessions, and headless checkout accept either a user JWT or a Business API key. Send API keys as a bearer token or through X-API-Key.

Authorization: Bearer vlt_live_...

# Also accepted on merchant resource endpoints
X-API-Key: vlt_live_...

JWT-only account settings

/api/apikeys, /api/settings/webhook, webhook dead letters, and replay require an authenticated user JWT. API keys cannot create/revoke other keys or change webhook settings.

curl https://vulta.one/api/apikeys \
  -X POST \
  -H "Authorization: Bearer YOUR_USER_JWT" \
  -H "Content-Type: application/json" \
  -d '{ "label": "production store" }'

{
  "key": "vlt_live_...",
  "api_key": {
    "id": "uuid",
    "key_prefix": "vlt_live_a1b2c3",
    "label": "production store",
    "created_at": "..."
  }
}

The raw key is returned once. Store it in a server secret manager. Never send it to Vulta support or commit it to source control.

02 / Hosted checkout

Create one checkout URL per order.

Recommended SaaS and e-commerce flow. Your backend creates the session; the payer opens the returned Vulta checkout; your server fulfills from the signed webhook.

1. Create the session from your backend

curl https://vulta.one/api/checkout/sessions \
  -H "Authorization: Bearer vlt_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "amount_fiat": "29.00",
    "fiat_currency": "USD",
    "external_reference_id": "order_123",
    "use_all_payout_destinations": true,
    "payer_email": "customer@example.com"
  }'
amount_fiatyesPositive decimal string. Do not send a JSON number.
fiat_currencyyesUppercase currency code such as USD or EUR.
external_reference_idrecommendedYour order or subscription ID. Returned unchanged in the webhook.
use_all_payout_destinationsone ofSet true, or provide payout_destination_ids instead. Do not provide both.
payer_emailoptionalStored with the request for merchant workflow.
expires_atoptionalFuture RFC 3339 timestamp. Omit for the server default.
{
  "id": "018f4f3e-2e8b-7a21-9f0c-0b6f56a7d3b1",
  "checkout_url": "https://vulta.one/pay/018f4f3e-2e8b-7a21-9f0c-0b6f56a7d3b1",
  "payment_request": {
    "id": "018f4f3e-2e8b-7a21-9f0c-0b6f56a7d3b1",
    "pricing_mode": "fixed_fiat",
    "amount_fiat": "29.00",
    "fiat_currency": "USD",
    "status": "OPEN",
    "expires_at": "..."
  }
}

2. Redirect from a payer action

// Browser code calls your backend, never Vulta with a secret key.
const response = await fetch('/api/store/orders/123/vulta-checkout', {
  method: 'POST'
});
if (!response.ok) throw new Error('Checkout creation failed');

const { checkout_url } = await response.json();
window.location.assign(checkout_url);

3. Fulfill only after payment.confirmed

Browser redirects are not proof of payment

Do not fulfill from the return page, widget success message, polling alone, or card-provider UI. Commit fulfillment only after a valid signed payment.confirmed webhook.

03 / Widget & iframe

Keep checkout inside the merchant site.

Embeds use a reusable payment link ID—not an API key and not a payment-request ID. Create the payment link first, then use one of these two supported integrations.

Automatic widget button

<script src="https://vulta.one/widget.js"></script>
<button type="button" data-vulta-link="PAYMENT_LINK_ID">
  Pay now
</button>

The widget finds clickable elements carrying data-vulta-link and opens a secure modal. Putting that attribute on the script tag alone does nothing.

Programmatic widget

<script src="https://vulta.one/widget.js"></script>
<button type="button" onclick="openVultaCheckout()">Pay now</button>
<script>
  function openVultaCheckout() {
    Vulta.open('PAYMENT_LINK_ID', {
      onSuccess: function (event) {
        // UI signal only. Fulfill the order from the signed server webhook.
        console.log(event.paymentRequestId);
      }
    });
  }
</script>

onSuccess is a user-interface signal. Always verify and fulfill through the server webhook.

Direct iframe

<iframe
  src="https://pay.vulta.one/embed/link/PAYMENT_LINK_ID?parentOrigin=https%3A%2F%2Fshop.example"
  title="Secure checkout"
  width="420"
  height="680"
  style="border:0;border-radius:24px"
  sandbox="allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox allow-same-origin"
  allow="payment"
></iframe>

Use pay.vulta.one/embed/link/…. The hosted /pay/… route deliberately blocks framing.

Restrict embed origins

Add exact HTTPS origins through allowed_embed_origins on the payment link, for example https://shop.example. Pass the same origin in the URL-encoded parentOrigin query parameter.

04 / Headless checkout

Build the payer interface yourself.

Business merchants can create an already-selected crypto transaction or a card-to-crypto transaction without showing Vulta checkout UI.

Crypto: receive the exact address and amount

curl https://vulta.one/api/checkout/crypto \
  -H "Authorization: Bearer vlt_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "network": "TRON",
    "asset": "USDT",
    "amount_crypto": "150.00",
    "external_reference_id": "order_456"
  }'
{
  "payment_request_id": "uuid",
  "payment_request_option_id": "uuid",
  "address": "TJRyWwFs9wTFGZg3JbrVriFbNfCug5tDeC",
  "network": "TRON",
  "asset": "USDT",
  "amount_fiat": null,
  "fiat_currency": null,
  "amount_crypto": "150.00",
  "encoded_amount": null,
  "amount_to_send": "150.00"
}

Send either amount_crypto, or amount_fiat with fiat_currency. Show the payer address and amount_to_send. Wallet pools may add a small encoded suffix for deterministic matching.

400

Invalid or missing fields.

409

No pool address available for this amount.

422

No active destination for the network and asset.

Card: receive a provider checkout URL

curl https://vulta.one/api/checkout/card \
  -H "Authorization: Bearer vlt_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "amount_fiat": "75.00",
    "currency": "EUR",
    "external_reference_id": "order_789"
  }'
{
  "provider_url": "https://provider.example/...",
  "payment_request_id": "uuid",
  "provider": "moonpay"
}

currency defaults to USD. provider is optional; omit it for routing. Provider availability and minimums change by currency and region. Query GET /api/checkout/providers?country=US&currency=USD for the current catalog.

Card settlement rule

Card checkout requires an active Polygon USDC destination. Vulta confirms only after callback authentication, sufficient forwarded value, and a successful Polygon USDC transfer to that merchant destination. A provider completion screen alone does not confirm an order.

Provider URL

The returned URL may be a direct provider URL or an approved PayGate checkout intermediary. It contains no Vulta checkout UI. Card/on-ramp providers may require payer-side KYC or additional verification.

Invalid input returns 400. Setup or provider-routing failures—including a missing Polygon USDC destination—currently return 502 with an error message.

05 / Merchant resources

Configure wallets and reusable payment links.

These endpoints accept JWT or API-key authentication. IDs always belong to the authenticated merchant workspace.

Payout destinations

curl https://vulta.one/api/payout-destinations \
  -H "Authorization: Bearer vlt_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "network": "Polygon",
    "asset": "USDC",
    "address": "0xYourWallet",
    "entry_method": "manual",
    "label": "Store settlements"
  }'

Supported pairs: Bitcoin/BTC; Ethereum/ETH, USDT, USDC; Base/ETH, USDC; Arbitrum/ETH, USDT, USDC; Polygon/POL, USDT, USDC; BSC/BNB, USDT, USDC; Solana/SOL, USDT, USDC; TRON/TRX, USDT.

entry_method is manual or connected_wallet. DELETE deactivates the destination; it does not erase historical payment records.

Payment links

curl https://vulta.one/api/payment-links \
  -H "Authorization: Bearer vlt_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Pro plan",
    "pricing_mode": "fixed_fiat",
    "fixed_amount_fiat": "49.00",
    "fixed_amount_currency": "USD",
    "is_hybrid": true,
    "payout_destination_ids": ["POLYGON_USDC_DESTINATION_ID"],
    "allowed_embed_origins": ["https://shop.example"]
  }'

Pricing modes are fixed_fiat and open_amount. Every link needs at least one eligible destination. Card-only or hybrid links must include an active Polygon USDC destination.

One-off payment requests

curl https://vulta.one/api/payment-requests \
  -H "Authorization: Bearer vlt_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "one_off": {
      "pricing_mode": "fixed_fiat",
      "amount_fiat": "250.00",
      "fiat_currency": "USD",
      "external_reference_id": "invoice_1042",
      "use_all_payout_destinations": true
    }
  }'

This Max endpoint returns the payment-request object only. It creates an OPEN request; it does not select a crypto option. Use hosted checkout, or fetch the public request and call POST /api/payment-requests/{id}/select-option.

06 / Payment lifecycle

Treat CONFIRMED as the fulfillment boundary.

Not every payment passes through every intermediate state. Terminal and recovery states are included so merchant systems can model the complete contract.

OPEN

Created; no option selected.

OPTION_SELECTED

Payer selected a crypto destination.

PENDING_PAYMENT

Card/on-ramp checkout initiated.

PARTIALLY_DETECTED

Payment observed but incomplete.

PAID

Observed; confirmation threshold not reached.

CONFIRMED

Safe fulfillment boundary.

BOUNCED

Provider flow failed; payer may retry.

EXPIRED

Request can no longer be paid.

CANCELLED

Merchant/system cancelled request.

Payer runtime endpoints

These routes are public because they operate only on existing merchant-created IDs. They cannot create arbitrary merchant checkouts.

POST/api/payment-requests/from-link/{linkId}Create a payer request from a linkpublic
GET/api/payment-requests/{id}Read request and optionspublic
POST/api/payment-requests/{id}/select-optionLock the chosen optionpublic
GET/api/payment-requests/{id}/statusRead current request, options, and selectionpublic
POST/api/checkout/card/statusRead stored card settlement status by polling tokenpublic

Password-protected links

POST /api/payment-links/{id}/verify-password returns {"valid": true}. It does not issue a session token. The hosted flow sends the password when creating a request from the link; public request reads use the raw password in X-Link-Password.

07 / Webhooks

Verify, deduplicate, commit, then acknowledge.

Business webhooks use a durable database outbox. Delivery is at least once, so duplicate delivery is expected and must be safe.

Configure from a JWT-authenticated session

curl https://vulta.one/api/settings/webhook \
  -X PUT \
  -H "Authorization: Bearer YOUR_USER_JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "webhook_url": "https://shop.example/api/vulta/webhook",
    "webhook_secret": "replace-with-a-long-random-secret"
  }'

The URL must be public HTTPS and must not resolve to loopback or private infrastructure. A non-empty signing secret is required whenever the URL is enabled. The secret is write-only.

Event payload

{
  "event_id": "9ef824be-50d4-4b75-97c0-f6bc3cc39e5f",
  "event_type": "payment.confirmed",
  "occurred_at": "2026-07-24T12:00:00Z",
  "payment_request_id": "a1b2c3d4-e5f6-...",
  "external_reference_id": "order_123",
  "amount_fiat": "29.00",
  "fiat_currency": "USD",
  "tx_hash": "0xabc123...",
  "status": "CONFIRMED",
  "merchant_id": "c3d4e5f6-..."
}

event_id is stable across retries. tx_hash, fiat fields, and external_reference_id may be absent when the underlying request does not contain them.

Delivery headers

X-Vulta-Event-ID: <event_id>
X-Vulta-Timestamp: <unix-seconds>
X-Vulta-Signature-V2: <hex HMAC-SHA256(timestamp + "." + raw_body)>
X-Vulta-Signature: <legacy body-only hex HMAC>

Use Signature V2. Reject timestamps more than five minutes from your server clock. The body must be the exact raw bytes received—not parsed and re-serialized JSON.

Node.js verification

const crypto = require('crypto');
const express = require('express');
const app = express();

function validHexDigest(value) {
  return typeof value === 'string' && /^[0-9a-f]{64}$/i.test(value);
}

function verifyVultaWebhook(rawBody, headers, secret) {
  const timestamp = headers['x-vulta-timestamp'];
  const signature = headers['x-vulta-signature-v2'];
  const eventId = headers['x-vulta-event-id'];
  const unixTime = Number(timestamp);

  if (!eventId || !Number.isFinite(unixTime)) return false;
  if (Math.abs(Math.floor(Date.now() / 1000) - unixTime) > 300) return false;
  if (!validHexDigest(signature)) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(timestamp)
    .update('.')
    .update(rawBody)
    .digest();
  const provided = Buffer.from(signature, 'hex');

  return provided.length === expected.length &&
    crypto.timingSafeEqual(provided, expected);
}

app.post('/api/vulta/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
  if (!verifyVultaWebhook(req.body, req.headers, process.env.VULTA_WEBHOOK_SECRET)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const event = JSON.parse(req.body.toString('utf8'));

  // In one DB transaction:
  // 1. INSERT event.event_id into a UNIQUE column (ignore duplicates).
  // 2. Fulfill event.external_reference_id.
  // 3. Commit.

  return res.status(204).end();
});

Python verification

import hashlib
import hmac
import time

def verify_vulta_webhook(raw_body: bytes, headers, secret: str) -> bool:
    timestamp = headers.get("X-Vulta-Timestamp", "")
    signature = headers.get("X-Vulta-Signature-V2", "")
    event_id = headers.get("X-Vulta-Event-ID", "")

    try:
        if not event_id or abs(int(time.time()) - int(timestamp)) > 300:
            return False
        provided = bytes.fromhex(signature)
    except (TypeError, ValueError):
        return False

    expected = hmac.new(
        secret.encode(),
        timestamp.encode() + b"." + raw_body,
        hashlib.sha256,
    ).digest()
    return hmac.compare_digest(provided, expected)

Retries, duplicates, and replay

Delivery schedule

Vulta makes six total attempts: immediately, then after approximately 1, 5, 15, 30, and 60 minutes. Any HTTP 2xx acknowledges delivery. Redirects are not followed.

Idempotency

Store event_id under a database UNIQUE constraint in the same transaction as order fulfillment. Return 2xx only after that transaction commits.

GET/api/settings/webhook/dead-lettersList last 50 exhausted eventsJWT · Business
POST/api/settings/webhook/dead-letters/{eventId}/replayReset an exhausted event for deliveryJWT · Business

08 / Endpoint reference

Merchant and payer API directory.

Protected resource routes below accept JWT or API keys unless explicitly marked JWT-only.

Merchant resources

GET/api/payout-destinationsList receiving wallets
POST/api/payout-destinationsAdd receiving wallet
PATCH/api/payout-destinations/{id}Update mutable fields
DELETE/api/payout-destinations/{id}Deactivate wallet
GET/api/payment-linksList reusable links
POST/api/payment-linksCreate reusable link
GET/api/payment-links/{id}Get merchant link
PATCH/api/payment-links/{id}Update merchant link
DELETE/api/payment-links/{id}Deactivate merchant link
GET/api/payment-requestsList transactions
POST/api/payment-requestsCreate one-off requestMax+
POST/api/checkout/sessionsCreate hosted checkout URLMax+
POST/api/checkout/cryptoCreate selected crypto checkoutBusiness
POST/api/checkout/cardCreate card-provider checkoutBusiness

Catalog and account settings

GET/api/checkout/providersLive provider catalog and minimumspublic
GET/api/checkout/countriesProvider-supported countriespublic
GET/api/apikeysList API keysJWT · Business
POST/api/apikeysCreate API keyJWT · Business
DELETE/api/apikeys/{id}Revoke API keyJWT · Business
GET/api/settings/webhookRead webhook settingsJWT · Business
PUT/api/settings/webhookUpdate webhook URL and secretJWT · Business

Error and retry rules

400

Malformed or invalid request.

401

Missing, expired, or invalid credentials.

403

Current plan does not include the feature.

404

Resource or polling token not found.

409

State conflict or exhausted wallet-pool capacity.

422

No eligible crypto destination.

429

Rate limit exceeded.

500 / 502 / 503

Internal, provider, or temporary dependency failure.

Creation retries

Merchant creation endpoints do not currently provide general idempotency keys. Persist successful responses and avoid blind retries after a network timeout. external_reference_id is returned for correlation but is not a uniqueness key. The payer-only from-link route supports X-Idempotency-Key for 24 hours.

Current limits: global 120 requests/minute per process; authentication endpoints 10/minute per IP; payer from-link creation 10/minute per IP; password verification 5 attempts per 10 minutes per link.

Production checklist

Test one low-value card payment and one crypto payment before opening checkout to customers.

Verify the destination wallet receives funds, the event signature passes, duplicate delivery is harmless, and fulfillment commits before your endpoint returns 2xx.

Contact integration support