Billing

Saved cards

Request buyer consent to save a card at checkout, then charge that card later without the buyer present. Card entry stays on MeridaPay's hosted fields, so saving a card does not expand your PCI scope.

How it works

When you mint a checkout with save_payment_method: true, the buyer sees a consent notice on the checkout page before they pay:

"By completing this payment, you agree to save your card for future payments from this merchant."

On success, MeridaPay stores a token bound to your organization. The raw card number never touches your server. Once the checkout is paid, read the refs you need for future charges from GET /checkouts/{id}.

Mint a save-card checkout

Pass save_payment_method: true in the body of POST /checkouts. Everything else works the same as a normal checkout.

typescript
// Your backend
const res = await fetch("https://api.meridapay.com/checkouts", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.MERIDA_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
  },
  body: JSON.stringify({
    product_id: "<product-id>",
    customer_email: "[email protected]",
    success_url: "https://app.example.com/thanks",
    save_payment_method: true,
  }),
})
const { url } = await res.json()
return Response.redirect(url, 303)

With the TypeScript SDK:

typescript
const { data } = await merida.POST("/checkouts", {
  body: {
    product_id: "<product-id>",
    customer_email: "[email protected]",
    success_url: "https://app.example.com/thanks",
    save_payment_method: true,
  },
  headers: { "idempotency-key": crypto.randomUUID() },
})
return Response.redirect(data.url, 303)

Read the saved card refs

After the buyer pays, fetch the checkout to get the refs you need to charge the card later. Both fields are null on an unpaid checkout and on any checkout where save_payment_method was not set.

bash
curl https://api.meridapay.com/checkouts/<id> \
  -H "Authorization: Bearer $MERIDA_API_KEY"
json
{
  "id": "e7b5…",
  "status": "paid",
  "customer_ref": "cus_…",
  "payment_method_ref": "pm_…",
  "card_brand": "visa",
  "card_last4": "4242"
}

Store customer_ref and payment_method_ref in your database alongside the buyer's record. You will pass them back to charge the card later.

Charge a saved card later (off-session)

When the buyer is not present, call POST /partner/practices/{id}/charges/off-session with the saved refs. The call is synchronous and the response carries the outcome.

bash
curl -X POST \
  https://api.meridapay.com/partner/practices/<practice-id>/charges/off-session \
  -H "Authorization: Bearer $MERIDA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "customer_ref":       "cus_…",
    "payment_method_ref": "pm_…",
    "amount":             "99.00",
    "label":              "Monthly plan, October 2026",
    "idempotency_key":    "charge-oct-2026-jane"
  }'

With the TypeScript SDK:

typescript
const { data } = await merida.POST(
  "/partner/practices/{id}/charges/off-session",
  {
    params: { path: { id: practiceId } },
    body: {
      customer_ref: savedCard.customerRef,
      payment_method_ref: savedCard.paymentMethodRef,
      amount: "99.00",
      label: "Monthly plan, October 2026",
      idempotency_key: "charge-oct-2026-jane",
    },
  }
)
customer_refstringrequired

From GET /checkouts/{id} on a paid save-card checkout.

payment_method_refstringrequired

Pairs with customer_ref, from the same checkout read.

amountdecimal stringrequired

Decimal major-units, e.g. "99.00". The currency matches the saved card's original checkout.

labelstringrequired

Shown as the statement descriptor. Use something the buyer will recognize, like the plan name and billing period.

idempotency_keystringrequired

A unique key you choose, 8 to 64 characters. The same key always maps to the same charge so retries never double-charge.

succeededstatus

The charge went through. Funds will settle to your account.

failedstatus

The charge was declined. Check error_code (e.g. card_declined, expired_card) and decide whether to retry or ask the buyer to update their card.

requires_actionstatus

The card issuer requires additional authentication. Off-session charges cannot complete 3DS challenges, so treat this like a decline and request new card details from the buyer.

On a succeeded charge the response also carries a payment_id. Store it if you may need to refund this charge later. It is the identifier the refund endpoint takes, and the only one that works for an off-session charge since it has no checkout. See Refunds.

View and manage saved cards in the console

You do not need to write any code to see or act on saved cards from the dashboard. Open a customer record in the MeridaPay dashboard (or the embedded /manage console if your platform embeds it), find the Saved cards panel in the customer drawer, and you will see the masked card identity (brand and last 4 only) for every card on file.

From that same panel, a merchant with the charge capability (owner or admin by default) can trigger an off-session charge directly in the console. No API call is needed from your side.

Authorization requirement

You are responsible for obtaining and recording the buyer's written authorization before charging them off-session. The consent notice on the checkout page covers the save itself. For recurring or future charges, make sure your terms of service and checkout flow make it clear to the buyer what they are authorizing. MeridaPay records that the consent notice was shown and accepted, but the scope of the authorization is yours to define.

PCI scope

Card entry always happens on MeridaPay's hosted fields, whether through the hosted checkout page or the embedded overlay. MeridaPay stores a payment token, never the raw card number. This means enabling saved cards does not expand your PCI compliance scope.