Getting Started

Run an end-to-end ButterPay integration in 10 minutes.

Prerequisites

  • Node.js 18 or later
  • An EVM-compatible test wallet with a known address (MetaMask or any wallet that exposes an Arbitrum address)

1. Sign up

Create a merchant account. The response includes a JWT that you will use in the next two steps.

curl -X POST https://api.butterpay.io/v1/merchants \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme Store",
    "email": "you@example.com",
    "password": "s3cur3passw0rd"
  }'

Response

{
  "id": "mer_01hwz3k9x2f8b4c6e7g9h0j1km",
  "name": "Acme Store",
  "email": "you@example.com",
  "serviceFeeBps": 80,
  "createdAt": "2026-04-27T10:00:00.000Z",
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Save the token value. It is your JWT and is valid for the current session.

The signup endpoint rate-limits to 5 requests per minute per IP.


2. Configure receiving address

Before generating an API key you must set at least one receiving address — this is the wallet where settlement funds land. The endpoint accepts Authorization: Bearer <token> from the JWT returned in step 1, or an existing X-Api-Key.

curl -X PATCH https://api.butterpay.io/v1/merchants/me \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "receivingAddresses": {
      "arbitrum": "0xYourArbitrumWalletAddress"
    }
  }'

Response — the updated merchant object, including the receivingAddresses field.

You can include additional chains when they become live. For now only arbitrum is in production.


3. Generate API key

POST /v1/merchants/me/generate-key issues your secret API key. The plain-text value is returned exactly once — the database stores only a hash. Copy it immediately.

curl -X POST https://api.butterpay.io/v1/merchants/me/generate-key \
  -H "Authorization: Bearer <token>"

Response

{
  "apiKey": "bp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}

All subsequent server-to-server calls use X-Api-Key: <apiKey> instead of the JWT.

To rotate a compromised key use POST /v1/merchants/me/rotate-key with a JWT (Dashboard login only). Rotation invalidates the previous key immediately.


4. Create your first invoice

Invoices are USD-denominated. The customer selects a chain and token at payment time, so you do not need to specify either upfront.

curl -X POST https://api.butterpay.io/v1/invoices \
  -H "X-Api-Key: <apiKey>" \
  -H "Content-Type: application/json" \
  -d '{
    "amountUsd": "49.99",
    "merchantOrderId": "order_001",
    "description": "Pro plan — monthly",
    "webhookUrl": "https://your-server.example.com/webhooks/butterpay",
    "redirectUrl": "https://your-server.example.com/thank-you"
  }'

Body fields

Field Required Description
amountUsd Yes Invoice amount as a decimal string, e.g. "49.99"
merchantOrderId No Your internal order ID; returned on the webhook
description No Shown on the hosted payment page
webhookUrl No Overrides the merchant-level default for this invoice
redirectUrl No Where to send the customer after successful payment
metadata No Arbitrary key/value object stored on the invoice

Response

{
  "id": "inv_01hwz4m8y3g9c5d7f8h0j2kn",
  "amount": "49.99",
  "description": "Pro plan — monthly",
  "status": "pending",
  "chain": null,
  "expiresAt": "2026-04-27T11:00:00.000Z",
  "createdAt": "2026-04-27T10:00:00.000Z",
  "redirectUrl": "https://your-server.example.com/thank-you",
  "merchantName": "Acme Store",
  "merchantOrderId": "order_001",
  "webhookUrl": "https://your-server.example.com/webhooks/butterpay"
}

The id field is the invoice identifier used to build the payment URL.


Construct the hosted payment URL from the invoice id:

https://pay.butterpay.io/pay/<invoice.id>

Example:

https://pay.butterpay.io/pay/inv_01hwz4m8y3g9c5d7f8h0j2kn

Send this URL to your customer by email, redirect them to it, or embed it as a button. The payment page is fully hosted — the customer connects a wallet, picks a supported chain and token (USDC or USDT on Arbitrum), and signs the on-chain transaction. Settlement is direct from the customer's wallet to your configured receiving address; ButterPay never holds funds.


6. Handle the webhook

When payment is confirmed on-chain, ButterPay POSTs a payment.confirmed event to the webhookUrl you configured. The request includes two headers:

  • X-ButterPay-Signaturet=<unix_timestamp>,v1=<hmac_sha256_hex>
  • X-ButterPay-Event — event name, e.g. payment.confirmed

The HMAC is computed over "<timestamp>.<rawBody>" using your merchant webhook secret. Always verify before acting on the payload.

Minimal Express handler:

import express from "express";
import crypto from "crypto";

const app = express();

// Capture raw body — signature is over the raw bytes, not the parsed object
app.use("/webhooks/butterpay", express.raw({ type: "application/json" }));

app.post("/webhooks/butterpay", (req, res) => {
  const sigHeader = req.headers["x-butterpay-signature"];
  if (!sigHeader) return res.status(400).send("Missing signature");

  // Parse t= and v1= from the header
  const parts = Object.fromEntries(
    sigHeader.split(",").map((p) => p.split("="))
  );
  const timestamp = parts["t"];
  const receivedHmac = parts["v1"];

  if (!timestamp || !receivedHmac) {
    return res.status(400).send("Malformed signature header");
  }

  // Reject replays older than 5 minutes
  const age = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10);
  if (age > 300) return res.status(400).send("Timestamp too old");

  // Recompute HMAC
  const secret = process.env.BUTTERPAY_WEBHOOK_SECRET;
  const signedContent = `${timestamp}.${req.body.toString("utf8")}`;
  const expected = crypto
    .createHmac("sha256", secret)
    .update(signedContent)
    .digest("hex");

  // Constant-time comparison to prevent timing attacks
  const expectedBuf = Buffer.from(expected, "hex");
  const receivedBuf = Buffer.from(receivedHmac, "hex");
  if (
    expectedBuf.length !== receivedBuf.length ||
    !crypto.timingSafeEqual(expectedBuf, receivedBuf)
  ) {
    return res.status(401).send("Invalid signature");
  }

  const payload = JSON.parse(req.body.toString("utf8"));
  const { event, invoiceId, merchantOrderId } = payload;

  console.log(`Verified webhook: event=${event} invoice=${invoiceId} order=${merchantOrderId}`);

  // Fulfill the order here
  res.sendStatus(200);
});

app.listen(3000);

Complete copy-paste script

The script below performs steps 1–4 end-to-end: sign up, configure a receiving address, generate an API key, and create an invoice. Copy it to script.mjs and run node script.mjs.

// script.mjs — ButterPay end-to-end quickstart
// Usage: node script.mjs
// Requires Node.js 18+

const BASE = process.env.BUTTERPAY_API ?? "https://api.butterpay.io";

async function post(path, body, headers = {}) {
  const res = await fetch(`${BASE}${path}`, {
    method: "POST",
    headers: { "Content-Type": "application/json", ...headers },
    body: JSON.stringify(body),
  });
  if (!res.ok) {
    const err = await res.text();
    throw new Error(`POST ${path}${res.status}: ${err}`);
  }
  return res.json();
}

async function patch(path, body, headers = {}) {
  const res = await fetch(`${BASE}${path}`, {
    method: "PATCH",
    headers: { "Content-Type": "application/json", ...headers },
    body: JSON.stringify(body),
  });
  if (!res.ok) {
    const err = await res.text();
    throw new Error(`PATCH ${path}${res.status}: ${err}`);
  }
  return res.json();
}

async function main() {
  // Step 1: Sign up
  console.log("1. Creating merchant account...");
  const signup = await post("/v1/merchants", {
    name: "Acme Store",
    email: `acme+${Date.now()}@example.com`,
    password: "s3cur3passw0rd",
  });
  const { token, id: merchantId } = signup;
  console.log(`   Merchant ID: ${merchantId}`);

  const bearerHeaders = { Authorization: `Bearer ${token}` };

  // Step 2: Configure receiving address
  console.log("2. Configuring receiving address...");
  await patch(
    "/v1/merchants/me",
    {
      receivingAddresses: {
        arbitrum: "0x1234567890abcdef1234567890abcdef12345678",
      },
    },
    bearerHeaders
  );
  console.log("   Receiving address set.");

  // Step 3: Generate API key
  console.log("3. Generating API key...");
  const keyResult = await post("/v1/merchants/me/generate-key", {}, bearerHeaders);
  const apiKey = keyResult.apiKey;
  console.log(`   API key: ${apiKey}`);
  console.log("   Store this securely — it will not be shown again.");

  const apiKeyHeaders = { "X-Api-Key": apiKey };

  // Step 4: Create an invoice
  console.log("4. Creating invoice...");
  const invoice = await post(
    "/v1/invoices",
    {
      amountUsd: "49.99",
      merchantOrderId: "order_001",
      description: "Pro plan — monthly",
      webhookUrl: "https://your-server.example.com/webhooks/butterpay",
      redirectUrl: "https://your-server.example.com/thank-you",
    },
    apiKeyHeaders
  );
  console.log(`   Invoice ID: ${invoice.id}`);
  console.log(`   Payment URL: https://pay.butterpay.io/pay/${invoice.id}`);
}

main().catch((err) => {
  console.error(err.message);
  process.exit(1);
});

Next steps

  • Authentication — full auth modes: JWT vs API key, key rotation, session tokens
  • Webhooks — full event reference, retry policy, and webhook logs
  • API Reference — complete invoice endpoint reference

results matching ""

    No results matching ""