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:writescope. 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
webhookUrlsupplied 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.
| Event | Meaning |
|---|---|
channel_created | The payer accepted a quote and MakePay created the payment/deposit channel. |
status_changed | The payment session moved to a different processor status. This is the primary order-reconciliation event. |
settlement_updated | Settlement amount, asset, variance, or resolution data changed after reconciliation. |
payment_cancelled_by_payer | The payer cancelled an unfinished payment. |
payment_request_expired | The active request expired before a deposit was received. |
quote_expired | The selected quote expired before payment started. |
payment_link_expired | The payment link expired while its session was still waiting for the payer. |
payment_link_inactive | A paused or archived link ended an unfinished payment session. |
start_blocked_missing_company_wallet | Payment start was blocked because the merchant settlement wallet was unavailable. |
manual_resolution_completed | A merchant completed a supported manual underpayment or exception resolution. |
underpayment_difference_requested | MakePay requested collection of the remaining underpaid amount. |
refund_address_requested | MakePay requested a payer refund address for a payment-resolution workflow. |
refund_address_received | The payer submitted the requested refund address. |
subscription_status_changed | A 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.