Quickstart

Create your first JoPay payment request in 5 minutes.

Overview

You will create a payment request, get a pay_page_urlback, share it with a customer, and receive a webhook when they pay. Every step uses a single HTTP call — no SDK, no callback URLs, no OAuth dance.

Everything on this page works identically against a test key (jo_test_…) and a live key (jo_live_…). Start with a test key — test-mode requests are fully isolated from production data.

Prefer a terminal? npx @jopay/cli logingives you every call on this page as a command — see the CLI and MCP server pages.

Before you start

  1. Your partner has an account on JoPay and at least one merchant assigned to it. (If not, see Become a partner.)
  2. You have a partner API key. Create one in the partner portal (/portal/api-keys) or ask your admin to create one.
  3. You know the account_id you want to create the request for. Find it in the portal merchants list or via GET /api/v1/accounts.

Your first payment request

Create the request

Post a minimal JSON body with the merchant id and the amount in minor units (2500 = €25.00 for a partner configured in EUR).

curl -X POST https://<partner-slug>.jopay.app/api/v1/requests/create \
  -H "Authorization: Bearer jo_test_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "account_id": "b4d5e6f7-...",
    "fiat_amount_int": 2500,
    "memo": "Invoice #INV-2026-0042",
    "metadata": {
      "order_id": "ORD-12345"
    }
  }'

Read the response

You get back a request_id and a pay_page_url. The URL is what you share with the customer.

{
  "ok": true,
  "request_id": "a1b2c3d4-...",
  "status": "requested",
  "proof_status": "none",
  "pay_page_url": "https://pay.jopay.app/a1b2c3d4-...",
  "expires_at": "2026-04-17T18:00:00.000Z",
  "created_at": "2026-04-17T17:00:00.000Z",
  "amount": "2750000",
  "asset_id": "eip155:<chain_id>/erc20:<token_address>",
  "fiat_amount": 2500,
  "fiat_currency": "eur",
  "account_id": "b4d5e6f7-...",
  "metadata": { "order_id": "ORD-12345" }
}

Send the pay page URL to the customer

Email it, text it, embed it in a QR code — however you distribute checkout links today. The customer opens the URL and pays with any EVM wallet.

Receive the webhook

When the payment is verified on-chain, JoPay sends a payment.proof_verified event to your configured webhook URL. The payload includes your original metadata, so you correlate by metadata.order_id— no lookup table needed.

POST https://your-server.com/webhooks/jopay
x-jopay-event: payment.proof_verified
x-jopay-signature: v1=abc123...,t=1712345678
x-jopay-delivery: d1e2f3a4-...

{
  "event": "payment.proof_verified",
  "delivery_id": "d1e2f3a4-...",
  "request_id": "a1b2c3d4-...",
  "account_id": "b4d5e6f7-...",
  "proof_status": "verified",
  "tx_hash": "0x9f8e7d...",
  "payer_address": "0x1a2b3c...",
  "metadata": { "order_id": "ORD-12345" },
  "timestamp": "2026-04-05T14:35:00.000Z"
}

Same flow in JavaScript

const res = await fetch(
  "https://<partner-slug>.jopay.app/api/v1/requests/create",
  {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${process.env.JOPAY_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      account_id: ACCOUNT_ID,
      fiat_amount_int: 2500,
      memo: "Invoice #INV-2026-0042",
      metadata: { order_id: "ORD-12345" },
    }),
  },
);

const { request_id, pay_page_url } = await res.json();

// Share pay_page_url with the customer via email / SMS / QR.
// JoPay will webhook your server when payment verifies.
If you don't have a webhook endpoint ready, poll GET /api/v1/requests/:id until proof_status becomes verified. Webhooks are preferable — they remove the polling loop — but polling is a valid fallback during early integration.

Test mode vs live mode

Keys come in two flavors. The prefix is authoritative:

  • jo_test_…— hits sandbox data, fully isolated from production
  • jo_live_…— hits real merchant and payment data

Test-mode requests are tagged test_mode: true, filtered out of live views, and never visible to live keys. Use test keys in CI and staging so a buggy push can't create real payment requests.

You cannot mix modes in one call: a test key against a live merchant (or vice versa) returns 400 merchant_mode_mismatch.

What next?