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:

bash
bun add @meridapay/sdk
ts
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.

POST
/checkouts

Mints 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_iduuidrequired

A product in your org. Find it in Dashboard → Products.

customer_emailstringrequired

Email for the receipt and the customer record we create.

customer_namestring

Display name on the customer record.

external_customer_idstring

Your platform's user ID. Echoed on every webhook.

client_reference_idstring

Free-form reference: order ID, quote ID, anything you want back.

success_urlhttps URL

Buyer is redirected here after paying, with ?status=paid&payment_link_id=… appended.

cancel_urlhttps URL

Optional URL passed to the embed bridge for parent pages that render their own cancel flow.

discount_codestring

Pre-applied discount code.

currencystring

ISO 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

Authorizationstringrequired

Bearer mrd_test_… or Bearer mrd_live_…. The prefix decides which mode the request operates in.

Idempotency-Keystring

Same key + same org returns the prior checkout instead of creating a new one. Safe retry for network failures.

Example

bash
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:

json
{
  "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. See error.issues.
  • 400 checkout_create_failedProduct has no active price, a stale link expired, etc.
GET
/checkouts/{id}

Fetch a checkout's current state.

bash
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.

POST
/payments/{id}/refund

Refund 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

iduuidrequired

The payment id to refund.

Request body (optional)

amountdecimal string

How 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.

reasonstring

One of duplicate, fraudulent, requested_by_customer. Defaults to requested_by_customer.

Example

bash
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" }'
json
{
  "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.
POST
/buy-links/{product_id}/checkout

Mints 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 | slugrequired

Same product identifier the integrator /checkouts endpoint accepts: bare UUID, prod_<uuid>, or the product's slug.

Request body (optional)

discount_codestring

Pre-applied discount code.

success_urlhttps URL

Buyer redirect target after paying. {CHECKOUT_TOKEN} is substituted with the actual session token.

Example

bash
curl -X POST https://api.meridapay.com/buy-links/prod_8f5f7c7e-…/checkout \
  -H "Content-Type: application/json" \
  -d '{}'
json
{
  "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.
GET
/products

List products in your org. Pagination via limit (1–200, default 50) and offset.

bash
curl "https://api.meridapay.com/products?limit=50&offset=0" \
  -H "Authorization: Bearer $MERIDA_API_KEY"
json
{
  "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
}
GET
/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).

GET
/api/invoices/{invoiceId}/pdf

Returns 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.

bash
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 matches feature_key.
  • POST /events/ingest: body { events: [...] } (≤500). Each event: { name, customer_id? | external_customer_id?, value?, metadata?, timestamp?, external_id? }. external_id makes the call idempotent.
  • GET /customers/{id}/balances: {id} may be a Merida UUID or your external_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:

json
{
  "error": {
    "type": "invalid_request",
    "message": "product_id is required",
    "issues": [
      { "path": ["product_id"], "message": "Required" }
    ]
  }
}
error.typestring

Stable machine-readable code. One of unauthorized, not_found, invalid_request, checkout_create_failed.

error.messagestring

Human-readable summary. Safe to surface to your own logs.

error.issuesarray

Present on invalid_request: field-level validation errors with path and message.