Reference
API reference
REST over JSON. All requests need an Authorization: Bearer header. POSTs are idempotent when you send an Idempotency-Key.
SDK & interactive reference
Full interactive reference with try-it: api.meridapay.com/docs.
Type-safe TypeScript client on npm:
bun add @meridapay/sdk
import { createMeridaClient } from "@meridapay/sdk"
const merida = createMeridaClient({ apiKey: process.env.MERIDA_API_KEY! })
Base URL
https://api.meridapay.com
Both mrd_test_… and mrd_live_… keys use the same base URL. The server reads the key prefix and routes to the appropriate environment data.
/checkoutsMints a hosted checkout URL. The buyer redirects there, pays, and lands on
your success_url. The canonical payment event arrives via the
payment.succeeded webhook.
Request body
product_iduuidrequiredA product in your org. Find it in Dashboard → Products.
customer_emailstringrequiredEmail for the receipt and the customer record we create.
customer_namestringDisplay name on the customer record.
external_customer_idstringYour platform's user ID. Echoed on every webhook.
client_reference_idstringFree-form reference: order ID, quote ID, anything you want back.
success_urlhttps URLBuyer is redirected here after paying, with
?status=paid&payment_link_id=… appended.
cancel_urlhttps URLOptional URL passed to the embed bridge for parent pages that render their own cancel flow.
discount_codestringPre-applied discount code.
currencystringISO currency code to charge in, when the product is priced in more than one currency. Omit to let the hosted checkout pick the buyer's regional currency automatically (the buyer can still switch on the page). Pass this to pin one currency regardless of where the buyer is located.
metadataobject<string,string>Up to 50 KV pairs (keys ≤ 40 chars, values ≤ 500 chars). Echoed verbatim on every webhook.
Headers
AuthorizationstringrequiredBearer mrd_test_… or Bearer mrd_live_…. The
prefix decides which mode the request operates in.
Idempotency-KeystringSame key + same org returns the prior checkout instead of creating a new one. Safe retry for network failures.
Example
curl -X POST https://api.meridapay.com/checkouts \
-H "Authorization: Bearer $MERIDA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $YOUR_UNIQUE_KEY" \
-d '{
"product_id": "<your-product-uuid>",
"customer_email": "[email protected]",
"customer_name": "Jane Doe",
"external_customer_id": "user_123",
"client_reference_id": "order_456",
"success_url": "https://app.example.com/thanks",
"cancel_url": "https://app.example.com/cancel",
"metadata": {
"plan": "pro",
"org_id": "acme"
}
}'
201 response:
{
"id": "e7b5…",
"url": "https://pay.meridapay.com/abc123XYZ",
"product_id": "<your-product-uuid>",
"customer_email": "[email protected]",
"customer_name": "Jane Doe",
"external_customer_id": "user_123",
"client_reference_id": "order_456",
"metadata": { "plan": "pro", "org_id": "acme" },
"success_url": "https://app.example.com/thanks",
"cancel_url": "https://app.example.com/cancel",
"amount": "29.00",
"currency": "USD",
"status": "active",
"type": "one_time",
"expires_at": null
}
Errors
401 unauthorizedMissing or invalid Bearer token.404 not_foundProduct doesn't belong to the caller's org.400 invalid_requestBody failed schema validation. Seeerror.issues.400 checkout_create_failedProduct has no active price, a stale link expired, etc.
/checkouts/{id}Fetch a checkout's current state.
curl https://api.meridapay.com/checkouts/e7b5… \
-H "Authorization: Bearer $MERIDA_API_KEY"
Returns the same shape as the create response, with paid_at
populated once the buyer completes payment. Use webhooks as the trigger;
this endpoint is for dashboards and backfills, not polling.
/payments/{id}/refundRefund a captured payment, in full or in part. Funds return to the
customer's original payment method. Send an Idempotency-Key header so a
retried request returns the original refund instead of issuing a second one.
Path parameter
iduuidrequiredThe payment id to refund.
Request body (optional)
amountdecimal stringHow much to refund. Omit to refund the full remaining refundable balance. Never more than the captured amount minus what's already refunded, and the server always re-derives the ceiling — a larger value is rejected.
reasonstringOne of duplicate, fraudulent, requested_by_customer. Defaults to
requested_by_customer.
Example
curl -X POST https://api.meridapay.com/payments/8f5f7c7e-…/refund \
-H "Authorization: Bearer $MERIDA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: refund-2026-07-29-001" \
-d '{ "amount": "49.99" }'
{
"id": "b2c1…",
"payment_id": "8f5f7c7e-…-…",
"amount": "49.99",
"currency": "USD",
"status": "succeeded"
}
Response status
succeeded means the money is on its way back to the customer.
pending means the refund was accepted but the funds have not
landed back yet, which happens with some payment methods that take a day
or two to settle. A pending refund completes on its own. Re-fetch the
payment to read its final state. The refundable balance already reserves a
pending refund, so you cannot refund the same amount twice while one is in
flight.
Errors
404 not_foundPayment not found or not in your organization.400 validation_errorAmount exceeds the refundable balance, or the payment has no linked charge to reverse.429 rate_limitedToo many refund requests in a short window.
/buy-links/{product_id}/checkoutMints a fresh checkout session for a public product. Unauthenticated:
the URL itself is the credential, same model as a publicly shareable buy
link. Powers the permanent pay.meridapay.com/pay/<id> buyer URL that
the dashboard's Sell this product dialog hands merchants. Each call
returns a new single-use pay.meridapay.com/<token> URL.
Use POST /checkouts instead when you need to attach per-buyer context:
customer email, your external_customer_id, custom metadata, an
idempotency key.
Path parameter
product_iduuid | prefixed | slugrequiredSame product identifier the integrator /checkouts endpoint
accepts: bare UUID, prod_<uuid>, or the product's slug.
Request body (optional)
discount_codestringPre-applied discount code.
success_urlhttps URLBuyer redirect target after paying. {CHECKOUT_TOKEN} is
substituted with the actual session token.
Example
curl -X POST https://api.meridapay.com/buy-links/prod_8f5f7c7e-…/checkout \
-H "Content-Type: application/json" \
-d '{}'
{
"id": "e7b5…",
"url": "https://pay.meridapay.com/abc123XYZ",
"token": "abc123XYZ",
"product_id": "prod_8f5f7c7e-…-…",
"amount": "29.00",
"currency": "USD",
"status": "active",
"type": "one_time"
}
Errors
404 not_foundProduct is missing, archived, or not published.400 checkout_create_failedProduct has no active price, currency-conflict on the discount code, etc.
/productsList products in your org. Pagination via limit (1–200, default 50) and
offset.
curl "https://api.meridapay.com/products?limit=50&offset=0" \
-H "Authorization: Bearer $MERIDA_API_KEY"
{
"data": [
{
"id": "6bec3118-7294-4c7b-ba78-b3e024c82aa4",
"name": "Saige Pro",
"description": "The quiet kit.",
"prices": [
{ "id": "p_…", "amount": "79.00", "currency": "USD", "type": "recurring", "interval": "month" }
]
}
],
"limit": 50,
"offset": 0,
"has_more": false
}
/products/{id}Fetch a single product with all its prices. 404 if the ID isn't in your org (indistinguishable from a non-existent ID).
/api/invoices/{invoiceId}/pdfReturns a printable HTML invoice (Content-Type: text/html). Print-to-PDF in
the browser, or feed through a headless renderer if you need server-side PDF
output. Org-scoped: a key from a different org gets 404.
curl https://meridapay.com/api/invoices/<invoice-id>/pdf \
-H "Authorization: Bearer $MERIDA_API_KEY" \
-o invoice.html
See Invoices for the wider lifecycle.
Portal endpoints
Token-authenticated self-service endpoints. The token is in the URL path,
no Bearer header. Mint a token from the dashboard and email the
/portal/<token> URL to the customer, or call these endpoints directly from
your own backend if you want to surface cancel / pause / resume in your own
UI.
Full request/response shapes live on the Customer portal page.
POST /portal/<token>/cancel: body{ subscription_id, mode? }POST /portal/<token>/pause: body{ subscription_id, resumes_at?, keep_invoices? }POST /portal/<token>/resume: body{ subscription_id }POST /portal/<token>/uncancel: body{ subscription_id }
Cancel fires subscription.canceled. Pause, resume, and uncancel all fire subscription.updated.
Entitlements
Feature gating + metered usage. Full guide: Entitlements. Full request/response schemas at api.meridapay.com/docs.
POST /check: body{ feature_key, customer_id? | external_customer_id?, value? }. Returns{ allowed, balance, limit, consumed, remaining, reason }.POST /track: body{ feature_key, customer_id? | external_customer_id?, value?, idempotency_key?, metadata? }. Records usage against the meter whose slug matchesfeature_key.POST /events/ingest: body{ events: [...] }(≤500). Each event:{ name, customer_id? | external_customer_id?, value?, metadata?, timestamp?, external_id? }.external_idmakes the call idempotent.GET /customers/{id}/balances:{id}may be a Merida UUID or yourexternal_customer_id. Returns one row per (customer, meter) for the current period.GET /meters·POST /meters·GET|PATCH|DELETE /meters/{id}: meter CRUD.GET|POST /products/{id}/entitlements·DELETE /products/{id}/entitlements/{entitlementId}: attach features to products.
Error shape
Every 4xx / 5xx response has the same body:
{
"error": {
"type": "invalid_request",
"message": "product_id is required",
"issues": [
{ "path": ["product_id"], "message": "Required" }
]
}
}
error.typestringStable machine-readable code. One of unauthorized,
not_found, invalid_request,
checkout_create_failed.
error.messagestringHuman-readable summary. Safe to surface to your own logs.
error.issuesarrayPresent on invalid_request: field-level validation errors
with path and message.
Next up
Webhooks →