Quickstart

Create your first JoPay payment request in 5 minutes.

Overview

You will create a payment request, get a pay_page_url back, 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.

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 merchant_id you want to create the request for. Find it in the portal merchants list or via GET /api/v1/merchants.

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 '{
    "merchant_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": {
    "fiat_int": 2500,
    "fiat_code": "EUR",
    "usdc_micro": "2750000"
  },
  "merchant_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-...",
  "merchant_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({
      merchant_id: MERCHANT_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?