API Reference

Webhooks

Delivery headers, event payloads, retries, and delivery logs.

Callback URL

Choose the webhook ownership model that matches the credential:

  • OAuth integrations create a grant-scoped subscription with the makepay:webhooks:write scope. It receives only events attributed to payment links created by that grant, so two stores connected to the same company do not overwrite or receive each other's callbacks.
  • API-key integrations use the company MakePay callback in developer settings. MakePay product settings take precedence over the legacy shared MakeSwap callback.
  • Anonymous payment links use the per-link webhookUrl supplied at creation.

MakeCrypto returns a grant-subscription signing secret only on creation, reactivation, or explicit rotation. An anonymous link returns its per-link secret only when that link is created. Store either value immediately in the integration's server-side secret manager. Normal reads return only a masked summary and never reveal the secret again.

Delivery behavior

MakePay sends payment and subscription webhooks as POST requests with a JSON body. Failed deliveries are retried up to ten times at five-minute intervals. Manual resend is available from MakeCrypto webhook request logs.

Headers

content-type: application/json
user-agent: MakePay-Webhooks/1.0
x-makepay-delivery-id: 9f1c6cf4-8514-4ee5-80fd-8e8fe2b5e313
x-makepay-delivery-group-id: 9f1c6cf4-8514-4ee5-80fd-8e8fe2b5e313
x-makepay-delivery-origin: event
x-makepay-event: status_changed
x-makepay-attempt: 1
x-makepay-signature: t=1776556800,v1=7d4b3f...

Verify signatures

MakePay signs the exact raw JSON request body with HMAC-SHA256. The signature payload is:

{timestamp}.{raw_request_body}

The x-makepay-signature header contains the Unix timestamp and versioned digest:

t=1776556800,v1=<hex_hmac_sha256>

Example verification in Node.js:

import crypto from "node:crypto";

const WEBHOOK_SECRET = process.env.MAKEPAY_WEBHOOK_SECRET!;
const TOLERANCE_SECONDS = 300;

export function verifyMakePayWebhook(input: {
  rawBody: string;
  signatureHeader: string | null;
}) {
  if (!input.signatureHeader) {
    return false;
  }

  const parts = Object.fromEntries(
    input.signatureHeader.split(",").map((part) => {
      const [key, value] = part.trim().split("=");
      return [key, value];
    }),
  );

  const timestamp = Number(parts.t);
  const signature = parts.v1;

  if (!Number.isFinite(timestamp) || !signature) {
    return false;
  }

  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - timestamp) > TOLERANCE_SECONDS) {
    return false;
  }

  const expected = crypto
    .createHmac("sha256", WEBHOOK_SECRET)
    .update(`${timestamp}.${input.rawBody}`, "utf8")
    .digest("hex");
  const actualBuffer = Buffer.from(signature, "hex");
  const expectedBuffer = Buffer.from(expected, "hex");

  return (
    actualBuffer.length === expectedBuffer.length &&
    crypto.timingSafeEqual(actualBuffer, expectedBuffer)
  );
}

Read the raw body before parsing JSON. If your framework parses the body first, configure the webhook route to expose the raw request bytes and verify those bytes before trusting the payload.

Payment payload

{
  "deliveryId": "9f1c6cf4-8514-4ee5-80fd-8e8fe2b5e313",
  "type": "makepay.payment.status_changed",
  "createdAt": "2026-04-19T00:00:00.000Z",
  "event": {
    "type": "status_changed",
    "trigger": "payment_status_reconcile"
  },
  "paymentLink": {
    "id": "8d15bb78-d0f8-45ef-88d7-2a1f1f79644b",
    "uid": "01hzy4k6p4w9y2x7e2z7n8a2xm",
    "status": "active",
    "publicUrl": "https://makepay.io/payment/01hzy4k6p4w9y2x7e2z7n8a2xm",
    "expiresAt": "2026-04-19T12:00:00.000Z",
    "amount": "129.99",
    "currency": "USDT",
    "asset": "ETH.USDT-0xdac17f958d2ee523a2206206994597c13d831ec7",
    "label": "Website order #1042",
    "description": "Checkout for order #1042",
    "merchantOrderId": "order_1042",
    "clientEmail": "buyer@example.com",
    "clientId": null
  },
  "session": {
    "id": "5b55f0bb-0ac4-4f7c-a1d1-0d9af19c3bbd",
    "status": "complete",
    "previousStatus": "pending",
    "invoiceAsset": "USDT",
    "invoiceAmount": "129.99",
    "selectedSellAsset": "ETH",
    "requiredSellAmount": "0.04",
    "expectedBuyAmount": "129.99",
    "destinationAddress": "0xmerchant...",
    "depositAddress": "0xdeposit...",
    "channelId": "channel_123",
    "compositeChannelId": "ETH:channel_123",
    "sourceChain": "ETH",
    "expiresAt": "2026-04-19T00:30:00.000Z",
    "settlement": {},
    "errorMessage": null
  }
}

Event types

The x-makepay-event header and event.type contain the short event name. The top-level type is makepay.payment.<event> for payment events or makepay.subscription.status_changed for subscriptions.

EventMeaning
channel_createdThe payer accepted a quote and MakePay created the payment/deposit channel.
status_changedThe payment session moved to a different processor status. This is the primary order-reconciliation event.
settlement_updatedSettlement amount, asset, variance, or resolution data changed after reconciliation.
payment_cancelled_by_payerThe payer cancelled an unfinished payment.
payment_request_expiredThe active request expired before a deposit was received.
quote_expiredThe selected quote expired before payment started.
payment_link_expiredThe payment link expired while its session was still waiting for the payer.
payment_link_inactiveA paused or archived link ended an unfinished payment session.
start_blocked_missing_company_walletPayment start was blocked because the merchant settlement wallet was unavailable.
manual_resolution_completedA merchant completed a supported manual underpayment or exception resolution.
underpayment_difference_requestedMakePay requested collection of the remaining underpaid amount.
refund_address_requestedMakePay requested a payer refund address for a payment-resolution workflow.
refund_address_receivedThe payer submitted the requested refund address.
subscription_status_changedA subscription moved between active, paused, overdue, or cancelled.

quote_created and quote_refreshed are internal payment timeline events and are not sent to callback URLs. Consumers must ignore unknown future event types after signature verification and must never treat an unknown event as payment completion.

OAuth webhook subscriptions

OAuth integrations manage the subscription owned by their current authorization grant:

GET    /api/partner/v1/makepay/webhook-subscriptions/current
PUT    /api/partner/v1/makepay/webhook-subscriptions/current
DELETE /api/partner/v1/makepay/webhook-subscriptions/current

These routes do not accept API keys. Use a grant-bound access token with a matching DPoP proof when required by the token. GET requires makepay:webhooks:read; PUT and DELETE require makepay:webhooks:write.

Create or update the subscription with an absolute public HTTPS endpoint:

{
  "url": "https://api.shop.example/hooks/payment/makepay_makepay",
  "events": ["makepay.payment.*"],
  "description": "Medusa production store",
  "metadata": {
    "integration": "medusa",
    "installationId": "medusa_store_01hzy4k6p4w9y2x7e2z7n8a2xm"
  },
  "rotateSecret": false
}

url is canonical; endpointUrl, callbackUrl, and webhookUrl are accepted as aliases. events accepts one to 32 fully qualified makepay.payment.<event> names; omit it to subscribe to the makepay.payment.* wildcard. Grant subscriptions currently deliver payment events, not subscription-schedule events. description is at most 500 characters and metadata must be a JSON object no larger than 8 KB.

On first creation, reactivation, or rotateSecret: true, the response includes signingSecret once. Save it before discarding the response. GET returns secretLast4 and secret timestamps but never the full secret. An ordinary update with rotateSecret: false preserves the active secret.

For ordinary OAuth subscriptions, set active: false through PUT, or call DELETE, to disable the current subscription and erase its encrypted signing secret. Both operations keep a disabled non-secret record for connection status. Re-enabling creates a new secret.

The immutable official Medusa integration has a narrower historical-settlement exception. A Medusa disconnect or grant revocation stops OAuth API access and prevents new payment links from being associated with that installation, but it preserves a separately encrypted historical signing credential for links issued before the disconnect. That credential can authenticate only late or redelivered canonical payment-status events whose company, grant, installation, subscription, payment-link UID, and Medusa session all match the original payment. It cannot authorize API requests, create a checkout, or receive another installation's events.

This exception does not apply to developer applications or other native integrations. Medusa operators must keep the original encryption key, plugin tables, and webhook endpoint available until issued payments and queued deliveries are drained. Disconnecting or revoking tokens is not a signal to delete that settlement-verification history.

Send a stable Idempotency-Key on PUT and DELETE, especially when a PUT can create or rotate a secret. A byte-for-byte retry with the same canonical request replays the stored response with Idempotent-Replayed: true; when the original response contained a newly issued signingSecret, that secret is recovered and replayed only to the same OAuth grant and idempotency key. Reusing the key for a different request, or while the first request is still running, returns 409.

Payment links created with an OAuth token are tagged with that grant and its active subscription. OAuth list/detail calls are restricted to the same grant, and events are routed only to that installation. Existing API-key links keep using the company callback.

Subscription status payload

MakePay also sends a callback when a subscription status changes. The scheduler marks a subscription overdue when the oldest unpaid billing cycle is at least 24 hours past dueAt; once no unpaid cycle is more than 24 hours overdue, the scheduler moves the subscription back to active. Merchant and customer-portal status changes use the same callback.

{
  "deliveryId": "78c35c42-61fb-4dd3-94b7-2a7df998bb6f",
  "type": "makepay.subscription.status_changed",
  "createdAt": "2026-04-20T00:00:00.000Z",
  "event": {
    "type": "subscription_status_changed",
    "trigger": "subscription_scheduler"
  },
  "subscription": {
    "id": "f6b76460-a437-4a81-a59f-8fcbb18c0f0f",
    "uid": "sub_premium_001",
    "status": "overdue",
    "previousStatus": "active",
    "customerEmail": "buyer@example.com",
    "label": "Premium plan",
    "description": "Monthly subscription",
    "amountUsd": "49.99",
    "settlementAsset": "ETH.USDT-0xdac17f958d2ee523a2206206994597c13d831ec7",
    "cadence": "monthly",
    "billingIntervalUnit": "month",
    "billingIntervalCount": 1,
    "startAt": "2026-04-18T00:00:00.000Z",
    "timezone": "Asia/Dubai",
    "metadata": {
      "clientId": "client_1042"
    },
    "createdAt": "2026-04-18T00:00:00.000Z",
    "updatedAt": "2026-04-20T00:00:00.000Z"
  },
  "cycle": {
    "id": "f303b3b3-26d8-42bc-8c10-91fa1445f507",
    "subscriptionId": "f6b76460-a437-4a81-a59f-8fcbb18c0f0f",
    "sequence": 0,
    "dueAt": "2026-04-18T00:00:00.000Z",
    "amountUsd": "49.99",
    "paymentLinkId": "8d15bb78-d0f8-45ef-88d7-2a1f1f79644b",
    "paymentLinkUid": "01hzy4k6p4w9y2x7e2z7n8a2xm",
    "paymentUrl": "https://makepay.io/payment/01hzy4k6p4w9y2x7e2z7n8a2xm",
    "status": "overdue"
  },
  "data": {
    "previousStatus": "active",
    "nextStatus": "overdue",
    "reason": "cycle_one_day_overdue"
  }
}

Delivery logs API

Use the webhook request route to inspect deliveries and retries.

GET /api/partner/v1/makepay/webhook-requests?limit=100

Optional filters include paymentLinkUid, deliveryStatus, and search.

Need partner setup help?

Open the payment link details view in MakeCrypto to copy the generated snippets for a real payment UID, or return to the portal to manage merchant settings.

Open portal