Developers REST API

Build payments into your product.

Create a hosted checkout with one request, hear back through signed webhooks, and build the whole flow on testnets before you go live. Plain REST and JSON over HTTPS, so any language works.

No SDK neededTest mode on testnetsSigned webhooks

Illustrative. Keys and ids are shortened samples for Harbourview Markets Ltd.

Quickstart

From test keys to a verified webhook.

Three steps, all in test mode on Base Sepolia and Celo Sepolia. When it works, swap in your live keys and live webhook URL, and nothing else changes.

  1. 1

    Get your test keys

    Open Developers in your dashboard and make your keys. You get four at once: a publishable and a secret key for test, and the same pair for live. Each is shown once, so copy them somewhere safe.

    • Publishable keys create checkouts
    • Secret keys read payments, from your server only
  2. 2

    Create a payment

    Send the amount, your customer’s email and your own reference with your test publishable key, and send your customer to the paymentURL that comes back.

    Then check the result from your server with your secret key: GET /api/v1/payments/:paymentId/verify, or by your reference.

    POST /api/v1/payments
    curl -X POST https://api.chainpal.org/api/v1/payments \
      -H "Authorization: Bearer cp_pk_test_..." \
      -H "Content-Type: application/json" \
      -d '{
        "amount": 125000,
        "customerEmail": "chidi.okafor@mail.example",
        "reference": "ORDER10482",
        "callbackURL": "https://harbourview.example/paid",
        "failureURL": "https://harbourview.example/retry",
        "metadata": { "branch": "lekki" }
      }'

    200Response

    {
      "success": true,
      "message": "payment initialized successfully",
      "data": {
        "paymentId": "6708f3c2a1e4b7d9c0f12a34",
        "paymentURL": "https://pay.chainpal.org/c/h7Qm2xR4kT9a"
      }
    }
  3. 3

    Handle the webhook

    Add your test webhook URL, https only, and make a signing secret. Every delivery carries X-ChainPal-Timestamp and X-ChainPal-Signature: check one against the other before you act on the event.

    Node’s built-in crypto module is all it takes. No SDK, no dependencies.

    verify.js
    Node, built-in crypto only
    const crypto = require("node:crypto");
    
    // rawBody: the request body exactly as it arrived, before JSON.parse
    function isFromChainPal(rawBody, headers, secret) {
      const timestamp = headers["x-chainpal-timestamp"];
      const signature = String(headers["x-chainpal-signature"] || "");
    
      // optional: refuse deliveries more than five minutes old
      if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
    
      const expected = crypto
        .createHmac("sha256", secret)
        .update(`${timestamp}.${rawBody}`)
        .digest("hex");
    
      const a = Buffer.from(expected);
      const b = Buffer.from(signature.replace(/^v1=/, ""));
      return a.length === b.length && crypto.timingSafeEqual(a, b);
    }

Keys and security

Locked down by default.

Four keys with separate jobs, stored only as hashes, and secret keys you can tie to the servers you name.

Four keys, two environments

The prefix says which key it is and where it runs.

TestLive
PublishableCreates checkouts · 30 requests a minute cp_pk_test_cp_pk_live_
SecretReads payments, server only · 100 requests a minute cp_sk_test_cp_sk_live_

Each key is shown once, when it is made, and stored only as a hash. If one is lost, all four are replaced together.

IP allowlist

Tie your secret keys to your own servers.

Up to 3 addresses. With a list in place, a secret-key request from anywhere else gets 403 Forbidden.

Test mode on testnets

Test keys run on Base Sepolia and Celo Sepolia, and test payments settle to wallets only. Nothing in test touches live money.

Separate webhook URLs

One for test and one for live, https only, every delivery signed with your signing secret.

Look up by id or reference

Verify any payment by its id or by your own reference, which must be unique, so one order cannot open two checkouts.

Clear rate limits

30 requests a minute on a publishable key and 100 on a secret key. Past that, you get a 429 to back off on.

Webhooks

Trust every webhook you act on.

Each delivery is signed with HMAC-SHA256 over the timestamp and the raw body, using your signing secret. Change the body below and watch the signature stop matching. It is worked out in your browser.

A delivery to your serverPOST /hooks/chainpal
  1. 1
    Signed contentThe timestamp, a dot, then the raw body1791724200.{"id":"evt_8Kq2Lm9xR4tA","type":"payment.completed","environment":"live","createdAt":"2026-10-11T13:10:00Z","data":{"paymentId":"6708f3c2a1e4b7d9c0f12a34","clientReference":"ORDER10482","fiatAmount":"125000.00","currency":"NGN","cryptoAmount":"81.13","token":"USDC","network":"base","status":"completed"}}
  2. 2
    HMAC-SHA256 with your secretHex encoded6330e786012809fb245b0baf3aaa01a33e64befbb54c40fb110f95b2104b3bd6
  3. 3
    Compare with the headerAfter the v1=, in constant time6330e786012809fb245b0baf3aaa01a33e64befbb54c40fb110f95b2104b3bd6

Signatures match. It came from ChainPal and nothing changed on the way.

Three events

  • payment.receivedThe payment is seen on the network, before it settles. Status processing.
  • payment.completedThe payment has settled to your destination. Status completed.
  • payment.failedThe payment failed, or its address expired with nothing on it. Status failed or expired.

If your endpoint does not answer with a 2xx

  1. 1At once
  2. 230 s
  3. 32 min
  4. 415 min
  5. 51 h
  6. 66 h
  7. 724 h

Up to 7 attempts over about 31 hours, until your endpoint answers with a 2xx. Answer quickly, and do any slow work after you reply.

The secret, ids and amounts are samples. The headers, signing method and event names are the API’s own.

Reference

Four endpoints to start.

The base URL is https://api.chainpal.org/api/v1, and every request sends its key as a Bearer token. Every field, error and example is in the full reference.

EndpointKeyWhat it does
POST /paymentsPublishableCreates a checkout session and returns its payment URL.
GET /payments/:paymentId/verifySecretSays whether a payment was paid, with its status, amounts, coin and network.
GET /payments/reference/:reference/verifySecretThe same check, by the reference you sent.
GET /paymentsSecretLists your payments, a page at a time.
The full API referenceAuthentication, payments, webhooks, types and errors, with examples.chainpal.mintlify.app

Works with

Part of one platform.

Payments you create through the API settle by the same rules and land in the same records as everything else on ChainPal.

Checkout

The hosted page your requests create, and everything your customer sees on it.

Payouts

Route Checkout and API payments to your bank, your treasury or a wallet you hold.

Treasury

Keep what you collect in stablecoins, in a Safe owned by your business’s passkeys.

Questions

Developers, answered.

Stuck on something? Write to help@chainpal.org with what you sent and what came back.

Is there an SDK?

No, and you do not need one. The API is REST and JSON over HTTPS, so any HTTP client in any language works. The examples on this page use curl, plain HTTP and Node’s built-in crypto module.

How do test and live differ?

By the key you send. A request with a cp_pk_test_ or cp_sk_test_ key runs in test mode, on Base Sepolia and Celo Sepolia, and settles to wallets only. Test and live have their own webhook URLs, so you can run both side by side.

How do we avoid creating the same payment twice?

Send your own reference with each payment, such as your order number. It has to be unique, so retrying a request for the same order cannot open a second checkout, and you can look the payment up by that reference later.

What happens if our webhook endpoint is down?

We try again, up to 7 attempts over about 31 hours, backing off from 30 seconds to 24 hours. Meanwhile you can always check a payment directly with your secret key, by its id or by your reference.

What is in each event?

An event id, its type (payment.received, payment.completed or payment.failed), the environment, and the payment: its id, your reference, the amounts, the coin, the network, the status and your metadata.

Where is the full reference?

In the API docs, with every field, error and example for authentication, payments and webhooks.

Open for business

Build on rails you control.

Open an account, choose where each product settles and take your first payment in digital dollars. No setup fee, and no balance held on your behalf.