Developers

Create payment links from your own system.

One endpoint. Create an invoice, get a link, send it to your customer. Poll the invoice to see when it is paid.

Authentication

Create an API key in your dashboard (Developer API → Create API key). Send it as a bearer token.

Create an invoice

POST https://vestapayments.com/api/invoices
Authorization: Bearer vk_test_...
Content-Type: application/json

{ "amount": "250", "currency": "USDC", "description": "Order #1234", "order_id": "1234", "expires_in_minutes": 60 }

→ 201
{ "invoice_id": "inv_…", "payment_url": "https://vestapayments.com/pay/inv_…", "amount": "250", "currency": "USDC", "status": "PENDING", "expires_at": "…" }

order_id must be unique per merchant — sending the same one twice returns 409, so retries are safe.

Check status

GET https://vestapayments.com/api/invoices/inv_…

→ { "id": "inv_…", "status": "CONFIRMED", "amount": "250", "fee": "7.50", "merchant_amount": "242.50", "tx": { "hash": "0x…", "confirmations": 12 } }

Statuses: PENDING (awaiting payment), DETECTED (seen on-chain, confirming), UNDERPAID (a deposit arrived but is less than the amount — deposit.missing says how much is still due), CONFIRMED (final), PAID_AFTER_EXPIRY (final, but after the link expired), EXPIRED, MISMATCH (a payment was made that does not match the invoice — review manually).

Every invoice also carries deposit_address: the address a customer can send USDC to straight from an exchange, and a deposit object with what has arrived (received), what is still on its way (arriving), and kind (waiting, short, exact, over).

List your invoices

GET https://vestapayments.com/api/invoices
Authorization: Bearer vk_test_...

Webhooks

Instead of polling, register an HTTPS URL in the dashboard (Webhooks card) or with the API. Vesta then POSTs a JSON body to it whenever a payment changes state, and retries for two days (1, 5, 15 minutes, then 1, 3, 6, 12 and 24 hours) until your server answers with any 2xx status.

PUT https://vestapayments.com/api/merchants/webhook
Authorization: Bearer vk_test_...
{ "url": "https://your-shop.example/vesta-webhook" }

→ { "ok": true, "url": "https://your-shop.example/vesta-webhook", "secret": "whsec_…" }

POST https://vestapayments.com/api/merchants/webhook        # sends a "ping" right away and reports the result
GET  https://vestapayments.com/api/merchants/webhook        # url, secret and the last 20 deliveries
DELETE https://vestapayments.com/api/merchants/webhook

Events: payment.detected (seen on-chain, not yet final — do not ship), payment.underpaid (part of the amount arrived at the deposit address), payment.confirmed (final — ship; also sent for payments confirmed after expiry, with status PAID_AFTER_EXPIRY), payment.expired (the link ran out unpaid — a later payment still produces payment.confirmed), payment.mismatch (needs a human), ping (your test).

POST https://your-shop.example/vesta-webhook
Content-Type: application/json
Vesta-Event: payment.confirmed
Vesta-Delivery-Id: 7c1b…            # same id on every retry — ignore ids you have already handled
Vesta-Signature: t=1758400000,v1=5f2a…   # HMAC-SHA256 of "<t>.<raw body>" with your secret

{ "id": "7c1b…", "event": "payment.confirmed", "created_at": "2026-09-21T09:00:00.000Z", "livemode": true,
  "data": { "invoice": { "id": "inv_…", "order_id": "1234", "status": "CONFIRMED", "amount": "250", "fee": "7.50",
                         "merchant_amount": "242.50", "tx": { "hash": "0x…" }, "deposit": { … } } } }

Verify the signature before trusting anything in the body. Compute HMAC-SHA256 over the string t + "." + rawBody with your whsec_… secret, compare it to v1 with a constant-time comparison, and reject timestamps older than five minutes:

// Node.js
import { createHmac, timingSafeEqual } from "crypto";
export function verifyVesta(secret, header, rawBody) {
  const { t, v1 } = Object.fromEntries(header.split(",").map((kv) => kv.split("=")));
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return expected.length === v1.length && timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}

Answer 2xx quickly (do your work after responding, or within ten seconds). Treat the invoice status in the body as the truth — a payment.confirmed event can arrive before payment.detected if your server was down, and each event is sent at most once per invoice. To be certain, fetch GET /api/invoices/:id after a webhook.

Limits during the pilot