GracePay for developers.

Create invoices, take payments and record money that arrives from elsewhere, from your own software.

Documentation sections

Overview and quick start

The GracePay API is a REST API with JSON bodies. Your server creates invoices and checkout sessions. The checkout component takes the payment in the browser. Webhooks tell your server what happened.

Take a payment, end to end

  1. Server: POST /invoices with issue: true.
  2. Server: POST /checkout_sessions, then pass client_token to the browser.
  3. Browser: render <gracepay-checkout>.
  4. Webhook: payment.completed records the payment. invoice.paid closes the invoice when paid_minor reaches total_minor.
1. Create and issue an invoice
curl https://api.trygracepay.com/v1/invoices \
  -H "Authorization: Bearer $GRACEPAY_SECRET_KEY" \
  -H "Idempotency-Key: $UNIQUE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "issue": true,
    "location_id": "'"$LOCATION_ID"'",
    "currency": "USD",
    "due_date": "2026-11-13",
    "payer": { "name": "Jordan Example", "email": "jordan@example.com", "phone": "+15155550100" },
    "lines": [
      { "description": "Professional services", "category": "professional_services", "amount_minor": 450000, "taxable": false },
      { "description": "Casket", "category": "merchandise", "amount_minor": 234000, "taxable": false }
    ]
  }'
2. Create a checkout session
curl https://api.trygracepay.com/v1/checkout_sessions \
  -H "Authorization: Bearer $GRACEPAY_SECRET_KEY" \
  -H "Idempotency-Key: $UNIQUE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "invoice_id": "'"$INVOICE_ID"'",
    "allowed_origins": ["https://app.example.com"],
    "return_url": "https://app.example.com/cases/1042"
  }'
3. Render the component
<script type="module" src="https://app.trygracepay.com/v1/checkout.js"></script>
<gracepay-checkout client-token="cst_…"></gracepay-checkout>
4. Receive the webhook
{ "id": "evt_…", "type": "payment.completed", "api_version": "v1", "created": "2026-10-30T15:04:05Z", "livemode": true, "merchant_id": "mer_…", "data": { "object": { "id": "pay_…", "status": "completed" } } }

Money from elsewhere

  1. Record money expected from elsewhere with POST /other_payments.
  2. Mark it received or canceled as it resolves.
  3. Send whatever stays open through a checkout session.

Conventions

ItemContract
Base URLhttps://api.trygracepay.com/v1 for live keys. Test keys use the staging sandbox at https://app.stage.trygracepay.com/v1 (see Partner platforms › Sandbox).
EnvironmentsSelected by key. Test keys (gp_test_…) work against GracePay's test environment and never move money. Live keys (gp_live_…) work against production. Objects carry livemode.
AuthenticationAuthorization: Bearer <secret key>, server-side only.
Acting for a merchantPartner keys send GracePay-Merchant: mer_… for a merchant linked to the partner. Calls about the partner itself (merchant links, partner webhook endpoints) send no header. Merchant keys are scoped to themselves. Any other merchant's object returns 404.
IdempotencySend Idempotency-Key on every POST. It is required on refunds and other payments. Keys are retained 24 hours, except refund keys, which last as long as the refund. Same key and body returns the same object in its current state, so a retried unknown refund reports its outcome. Same key with a different body returns 409 idempotency_conflict.
MoneyInteger minor units in *_minor fields, with currency (USD). null means not yet reported, never zero.
TimeRFC 3339 UTC. Calendar dates (due_date, date_of_death) are YYYY-MM-DD.
IDsOpaque, prefixed strings. Do not parse them. Prefixes: mer_, loc_, inv_, cs_, pay_, re_, op_, we_, dlv_, evt_, req_.
Listslimit (1-100, default 25), starting_after, returning { "data": [], "has_more": false }. Newest first.
Additive changesNew fields, statuses and event types can appear within v1. Clients must tolerate unknown values.
Rate limits429 with Retry-After.

Keys and settings

Organization admins manage these in the GracePay app at Settings › Organization › Developers.

SettingWhat it does
API keysCreate, name and revoke merchant keys. The secret is shown once.
Allowed host namesThe exact https:// origins that may embed <gracepay-checkout>. A checkout session's allowed_origins must be a subset of these (plus the partner's own origins for a partner key). Also managed through /allowed_origins.
Connected platformsThe partners linked to the organization, with how and when they were linked. Admins can disconnect one at any time.
Webhook endpointsThe same operations as /webhook_endpoints, plus delivery history and retry.

Partner keys, which send GracePay-Merchant, are issued by GracePay on request through the access form below. See Partner platforms.

Errors

Error body
{
  "error": {
    "type": "invalid_request",
    "code": "amount_exceeds_balance",
    "message": "…",
    "param": "amount_minor",
    "request_id": "req_…"
  }
}
StatustypeMeaning
400, 422invalid_requestThe request is malformed or breaks a rule.
401authenticationMissing or invalid key.
403permissionIncludes capability_not_enabled.
404not_foundMissing, or not visible to this key.
409conflictIncludes idempotency_conflict and invalid_state.
429rate_limitedSlow down.
5xxapi_errorThe outcome may be unknown. Retry with the same Idempotency-Key.

Capabilities

GET /capabilities returns what is enabled for the merchant and environment. A call to anything not enabled returns 403 capability_not_enabled.

Values: enabled, not_enabled, pending_review.

GET /capabilities response
{
  "merchant_id": "mer_…",
  "livemode": true,
  "capabilities": {
    "card": "enabled",
    "ach": "enabled",
    "financing": "enabled",
    "other_payments": "enabled"
  }
}

Merchants

Partner keys use every call. A merchant key can read only its own merchant and locations.

CallPurpose
POST /merchantsCreate a merchant. Returns the merchant with onboarding_url.
GET /merchants/{id}, GET /merchantsRead and list.
GET /merchants/{id}/locationsList locations.

Request body

FieldDescription
legal_nameThe merchant's legal name.
emailThe merchant's email address.
external_refOptional. Your reference.
metadataOptional. Your data.

Location fields

id, name, address, support_phone.

Status

onboarding, active, restricted, suspended, closed. Every merchant completes onboarding and underwriting and accepts the payment processor's terms before it can take payments. Approval is not guaranteed.

Create a merchant
curl https://api.trygracepay.com/v1/merchants \
  -H "Authorization: Bearer $GRACEPAY_SECRET_KEY" \
  -H "Idempotency-Key: $UNIQUE_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "legal_name": "Example Funeral Home LLC", "email": "owner@example.com", "external_ref": "tenant-1042" }'

Invoices

CallPurpose
POST /invoicesCreate, and optionally issue with issue: true.
GET /invoices/{id}, GET /invoicesRead and list. Filters: status, location_id, external_ref.
POST /invoices/{id}/issueIssue a draft. Issued lines are immutable.
POST /invoices/{id}/voidVoid. Refused while money is applied or reserved, or while any other payment on the invoice is expected or received.

Request body

FieldDescription
location_idThe merchant location.
currencyUSD.
due_dateWhen the invoice is due.
memoA note on the invoice.
external_refYour reference. Can be used as a list filter.
metadataYour data.
payer{ name, email, phone }.
decedentOptional. { name, date_of_death }.
lines[]{ description, category, amount_minor, taxable }. 1-100 lines.
issuetrue issues the invoice when it is created.

category: professional_services, facilities_and_staff, vehicles, merchandise, cemetery, cash_advance, other.

Response fields

id, number, status, subtotal_minor, tax_minor, total_minor, paid_minor, covered_minor, balance_minor.

Status: draft, open, partially_paid, paid, void.

total_minor is what is owed overall: the invoice total less any credits the merchant's staff recorded, so subtotal_minor plus tax_minor can be more than total_minor. balance_minor is total_minor less paid_minor and covered_minor, never below zero.

POST /invoices response
{
  "id": "…",
  "number": "1042",
  "status": "open",
  "subtotal_minor": 684000,
  "tax_minor": 0,
  "total_minor": 684000,
  "paid_minor": 0,
  "covered_minor": 0,
  "balance_minor": 684000
}

Checkout sessions

CallPurpose
POST /checkout_sessionsCreate. Reserves the amount on the invoice.
GET /checkout_sessions/{id}Read.
POST /checkout_sessions/{id}/cancelCancel. Releases the reservation only once the session is verified closed without a payment.

Request body

FieldDescription
invoice_idThe invoice to collect on.
amount_minorDefault: open balance.
methods[]card, ach, financing. Default: all enabled.
allowed_origins[]Exact https:// origins. Must be a subset of the organization's allowed host names. For a partner key, the partner's own registered origins are allowed too.
return_urlOptional.
expires_inSeconds, 300-86400. Default 3600.
metadataYour data.

Response fields

FieldDescription
idThe session.
statusopen, processing, completed, expired, canceled.
client_tokenBrowser-safe. Passed to the component. Only while the session is open or processing; null once it is closed and in webhook event snapshots.
urlThe hosted page. Like client_token, only while the session is open or processing.
expires_atWhen the session expires.
payment_idThe resulting payment.
POST /checkout_sessions response
{
  "id": "…",
  "status": "open",
  "client_token": "cst_…",
  "url": "…",
  "expires_at": "2026-10-30T16:04:05Z",
  "payment_id": null
}

Removing an allowed origin applies to existing sessions: new frames from it are refused, and every open session whose allowed_origins contains it can no longer start a payment. Sessions without a started payment are canceled. Create a new session for the origins that remain.

cancel answers with the session's current state. While GracePay is still confirming with the payment processor that the session closed without a payment, it stays open and no payment can complete through it. It becomes canceled, with checkout_session.canceled, once that is confirmed. A completed or processing session cannot be canceled (409 invalid_state).

Payments

Payments are read only: GET /payments/{id}, GET /payments.

Filters: invoice_id, merchant_id, status, created_from, created_to.

Fields

id, invoice_id, checkout_session_id, status, method, amount_minor, captured_minor, fee_minor, net_minor, refunded_minor, currency, created, metadata.

Status

StatusMeaning
pendingThe payment has not finished.
completedThe payment succeeded.
finalizedIt settled at the processor. This does not mean a bank deposit arrived.
canceledThe payment was canceled.
GET /payments/{id} response
{
  "id": "pay_…",
  "invoice_id": "…",
  "checkout_session_id": "…",
  "status": "completed",
  "method": "card",
  "amount_minor": 684000,
  "captured_minor": 684000,
  "fee_minor": null,
  "net_minor": null,
  "refunded_minor": 0,
  "currency": "USD",
  "created": "2026-10-30T15:04:05Z",
  "metadata": {}
}

Refunds

CallPurpose
POST /payments/{id}/refundsBody: amount_minor (default: remaining), reason. Idempotency-Key required.
GET /refunds/{id}, GET /refundsRead and list.

Fields: id, payment_id, status, amount_minor, currency, reason, created.

Status: requested, received, completed, failed, reversed, unknown.

  • A refund of a financed payment starts as requested for review by the merchant's staff.
  • Any other refund requested through the API is approved by that request (the key acts for the merchant) and sent to the payment processor. It stays requested while it cannot be sent yet.
  • A refund the merchant's staff decline is failed.
  • On unknown, retry with the original key, which never expires for a refund, and never with a new one.
Refund part of a payment
curl https://api.trygracepay.com/v1/payments/$PAYMENT_ID/refunds \
  -H "Authorization: Bearer $GRACEPAY_SECRET_KEY" \
  -H "Idempotency-Key: $UNIQUE_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "amount_minor": 50000, "reason": "Service changed" }'

Other payments

An other payment is money for an invoice that arrives from somewhere other than GracePay checkout. GracePay does not move or verify this money. You report it.

CallPurpose
POST /other_paymentsRecord money expected or received. Idempotency-Key required.
GET /other_payments/{id}, GET /other_paymentsRead and list. Filters: invoice_id, status, external_ref.
POST /other_payments/{id}/receiveMark it received: expected to received.
POST /other_payments/{id}/cancelCancel it: expected to canceled.

Request body

FieldDescription
invoice_idThe invoice the money is for.
amount_minorGreater than zero. Not more than the invoice's open balance.
currencyUSD.
statusexpected (default) or received.
expected_dateOptional. YYYY-MM-DD.
sourceOptional. A label of up to 100 characters, shown to the merchant's staff.
referenceOptional. Up to 100 characters.
descriptionOptional. Up to 500 characters.
external_refYour reference. Can be used as a list filter.
metadataYour data.

Receive and cancel

CallBody
POST /other_payments/{id}/receiveamount_minor (optional): the amount actually received. Greater than zero and not more than the expected amount. Default: the expected amount. received_date (optional): YYYY-MM-DD. Default: today in UTC.
POST /other_payments/{id}/cancelreason (optional): up to 500 characters.

Response fields

id, invoice_id, status, amount_minor, received_minor (null until received), reversed_minor (null until received; how much of what was received the merchant's staff later reversed), currency, expected_date, received_date, source, reference, description, cancel_reason, external_ref, metadata, livemode, created.

Status

StatusMeaning
expectedCounts toward the invoice's covered_minor. The payer is not asked for that amount.
receivedCounts toward paid_minor.
canceledThe amount returns to the invoice balance.
reversedThe merchant's staff reversed everything that was received, for example a returned check. It counts toward neither covered_minor nor paid_minor, and the amount is back on the invoice balance.

Rules

  • When less than expected arrives, the difference returns to the invoice balance.
  • A received other payment cannot be canceled through the API. It returns 409 invalid_state.
  • A partial reversal keeps received. reversed_minor shows how much was reversed, and only the rest counts toward paid_minor.
  • invoice.paid fires, and the invoice becomes paid, only when paid_minor reaches total_minor. A balance of zero made up partly of expected other payments is not paid.
  • An invoice cannot be voided while any other payment on it is expected or received.
Record an expected payment
curl https://api.trygracepay.com/v1/other_payments \
  -H "Authorization: Bearer $GRACEPAY_SECRET_KEY" \
  -H "Idempotency-Key: $UNIQUE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "invoice_id": "'"$INVOICE_ID"'",
    "amount_minor": 300000,
    "currency": "USD",
    "expected_date": "2026-11-20",
    "source": "Paid outside checkout",
    "reference": "REF-2207"
  }'
POST /other_payments response
{
  "id": "op_…",
  "invoice_id": "inv_…",
  "status": "expected",
  "amount_minor": 300000,
  "received_minor": null,
  "currency": "USD",
  "expected_date": "2026-11-20",
  "received_date": null,
  "source": "Paid outside checkout",
  "reference": "REF-2207",
  "description": null,
  "cancel_reason": null,
  "external_ref": null,
  "metadata": {},
  "livemode": true,
  "created": "2026-11-02T15:04:05Z"
}
Mark it received
curl https://api.trygracepay.com/v1/other_payments/$OTHER_PAYMENT_ID/receive \
  -H "Authorization: Bearer $GRACEPAY_SECRET_KEY" \
  -H "Idempotency-Key: $UNIQUE_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "amount_minor": 300000, "received_date": "2026-11-18" }'

Webhook endpoints

CallPurpose
POST /webhook_endpointsBody: url (HTTPS), events[], description. The response contains secret (whsec_…), shown once.
GET, PATCH, DELETE /webhook_endpoints/{id}, GET /webhook_endpointsManage. PATCH takes url, events[], description, enabled.
POST /webhook_endpoints/{id}/rotate_secretNew secret. The previous one stays valid for 24 hours.
GET /webhook_deliveries, GET /webhook_deliveries/{id}History for 90 days. Filters: endpoint_id, event_id, status.
POST /webhook_deliveries/{id}/retrySend again.

Organization admins can do the same in Settings › Organization › Developers.

Delivery fields: id, endpoint_id, event_id, event_type, status (pending, succeeded, failed), attempt_count, response_status, response_body, error, created, last_attempt_at, next_attempt_at, completed_at, livemode, and merchant_id, the merchant the event is about (added within v1).

A partner key with GracePay-Merchant manages that merchant's endpoints. Without the header it manages the partner's own endpoints, which receive events for every linked merchant. See Partner platforms › Webhooks for every linked merchant.

Create an endpoint
curl https://api.trygracepay.com/v1/webhook_endpoints \
  -H "Authorization: Bearer $GRACEPAY_SECRET_KEY" \
  -H "Idempotency-Key: $UNIQUE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://app.example.com/webhooks/gracepay",
    "events": ["payment.completed", "invoice.paid"],
    "description": "Production payments"
  }'

Allowed origins

The exact https:// origins that may frame checkout for a merchant: the merchant's allowed host names. Merchant keys, and partner keys acting for a linked merchant with GracePay-Merchant, can manage them, for example for a funeral home's own website.

CallPurpose
GET /allowed_originsList the merchant's origins.
POST /allowed_originsBody: origin. Adding one that is already registered returns the existing row and keeps its owner.
DELETE /allowed_origins/{id}Remove it. Open checkout sessions on it can no longer start, and those without a started payment are canceled.
  • Exact origins only: scheme, host and port, no path. Wildcards are refused. Where each funeral home has its own subdomain, add each one for its merchant.
  • Up to 50 per merchant, whoever added them.
  • The merchant's admins see who added each one, for example "Added by" your platform's name, and can remove any of them.
  • Origins a partner added are removed when that partner is disconnected.
Allow a funeral home's site
curl https://api.trygracepay.com/v1/allowed_origins \
  -H "Authorization: Bearer $GRACEPAY_PARTNER_KEY" \
  -H "GracePay-Merchant: $MERCHANT_ID" \
  -H "Idempotency-Key: $UNIQUE_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "origin": "https://www.examplefuneralhome.com" }'

Webhooks

Request
POST /your/endpoint
Content-Type: application/json
GracePay-Signature: t=1793491200,v1=5f8a…
GracePay-Delivery: dlv_…
Body
{
  "id": "evt_…",
  "type": "payment.completed",
  "api_version": "v1",
  "created": "2026-10-30T15:04:05Z",
  "livemode": true,
  "merchant_id": "mer_…",
  "data": { "object": { "id": "pay_…", "status": "completed" } }
}

Verifying signatures

Compute lowercase hex HMAC-SHA256 with the endpoint secret over <t>.<exact raw body> and compare it to v1 in constant time. Do not parse or re-serialize the body first. During rotation, accept either secret. Do not reject on age: retries can be delivered days after t.

Node
import { createHmac, timingSafeEqual } from 'node:crypto';
import express from 'express';

// During rotation, list both the new secret and the previous one.
const secrets = [process.env.GRACEPAY_WEBHOOK_SECRET];

function verifySignature(rawBody, header, secrets) {
  const fields = Object.fromEntries((header ?? '').split(',').map((part) => part.split('=', 2)));
  if (!fields.t || !fields.v1) return false;
  const received = Buffer.from(fields.v1);
  const signed = Buffer.concat([Buffer.from(fields.t + '.'), rawBody]);
  return secrets.some((secret) => {
    const expected = Buffer.from(createHmac('sha256', secret).update(signed).digest('hex'));
    return expected.length === received.length && timingSafeEqual(expected, received);
  });
}

const app = express();

// express.raw keeps the exact bytes. Do not use a JSON body parser on this route.
app.post('/webhooks/gracepay', express.raw({ type: 'application/json' }), (req, res) => {
  if (!verifySignature(req.body, req.get('GracePay-Signature'), secrets)) return res.sendStatus(400);
  const event = JSON.parse(req.body);
  // Deduplicate on event.id, then handle the event.
  res.sendStatus(200);
});
Python
import hashlib
import hmac
import json
import os

from flask import Flask, abort, request

# During rotation, list both the new secret and the previous one.
SECRETS = [os.environ["GRACEPAY_WEBHOOK_SECRET"]]


def verify_signature(raw_body: bytes, header: str, secrets: list[str]) -> bool:
    fields = dict(part.split("=", 1) for part in header.split(",") if "=" in part)
    t, v1 = fields.get("t"), fields.get("v1")
    if not t or not v1:
        return False
    signed = t.encode() + b"." + raw_body
    return any(
        hmac.compare_digest(
            hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest().encode(),
            v1.encode(),
        )
        for secret in secrets
    )


app = Flask(__name__)


@app.post("/webhooks/gracepay")
def gracepay_webhook():
    # get_data returns the exact bytes. Verify before parsing.
    raw_body = request.get_data()
    if not verify_signature(raw_body, request.headers.get("GracePay-Signature", ""), SECRETS):
        abort(400)
    event = json.loads(raw_body)
    # Deduplicate on event["id"], then handle the event.
    return "", 200

Delivery

  • Any 2xx within 10 seconds acknowledges.
  • Otherwise deliveries retry with exponential backoff, at most one hour apart, for 7 days.
  • id is stable across retries, so deduplicate on it.
  • Order is not guaranteed, and a delayed event carries an older snapshot. GET the object and act on its current state, never on the event's. A payment counts as paid when its current status is completed or finalized.

Events

EventsWhen
merchant.status_changedOnboarding or standing changed.
invoice.issued, invoice.updated, invoice.paid, invoice.voidedInvoice lifecycle.
checkout_session.completed, checkout_session.expired, checkout_session.canceledSession lifecycle.
payment.pending, payment.completed, payment.finalized, payment.canceledPayment lifecycle. payment.completed is the fulfilment signal.
refund.received, refund.completed, refund.failed, refund.reversedRefund lifecycle.
other_payment.created, other_payment.received, other_payment.canceled, other_payment.reversedOther payment lifecycle.
merchant.linked, merchant.unlinkedA merchant was linked to or disconnected from your partner account. Partner endpoints only.

Checkout component

HTML
<script type="module" src="https://app.trygracepay.com/v1/checkout.js"></script>
<gracepay-checkout client-token="cst_…"></gracepay-checkout>

Attributes

AttributeRequiredPurpose
client-tokenYesclient_token from the checkout session. Single session, short-lived. Environment comes from the token.
appearanceNolight (default), dark.
suppress-autoexpandNoDo not auto-open the method when only one is offered.

Events

All events bubble and are composed.

EventdetailMeaning
gracepay-ready{ checkoutSessionId }Rendered.
gracepay-processing{ checkoutSessionId }The payment is being processed.
gracepay-success{ checkoutSessionId, paymentId }Show a confirmation.
gracepay-failed{ checkoutSessionId, code }The attempt failed. The customer can retry.
gracepay-unavailable{ checkoutSessionId, code }Cannot render. Offer the session's url.

Method: element.reload().

Browser events drive the interface only. Fulfil only from `payment.completed` or a server read.

Content Security Policy and origins

  • The element frames checkout from the origin its script was loaded from. Load it from https://app.trygracepay.com/v1/checkout.js and it frames https://app.trygracepay.com. Once https://js.trygracepay.com is published, the script from there frames https://checkout.trygracepay.com. In the sandbox, use https://app.stage.trygracepay.com for both.
  • A host page with a Content Security Policy adds the script's origin to script-src and the frame's origin to frame-src.
  • The page's exact origin must be in the session's allowed_origins and in the organization's allowed host names (or, for a session a partner key created, the partner's origins).
  • A surface that is not a web page with an https:// origin, such as a native tablet app, opens the session's url instead of the frame.

Apple Pay and PCI

  • Apple Pay is not offered inside the frame. Open the session's url as a top-level page to offer it where enabled.
  • Card and bank fields stay in processor-hosted frames. Confirm your PCI SAQ level with your acquirer or QSA.

React

React usage needs no package.

TSX
function Checkout({ token, onPaid }: { token: string; onPaid: () => void }) {
  const ref = useRef<HTMLElement>(null);
  useEffect(() => {
    const el = ref.current!;
    el.addEventListener('gracepay-success', onPaid);
    return () => el.removeEventListener('gracepay-success', onPaid);
  }, [onPaid]);
  return <gracepay-checkout ref={ref} client-token={token} />;
}

Partner platforms

A partner, such as a case management system, integrates once and acts for every funeral home linked to it. Funeral homes never paste GracePay keys into partner software.

Partner keys

  • GracePay issues partner keys on request, per environment: gp_test_… for the sandbox, gp_live_… for production. The secret is handed over once.
  • Calls for one merchant send GracePay-Merchant: mer_…. They work only while that merchant is linked to you. Otherwise the merchant and its objects return 404.
  • Calls about your partner account (merchant links, your own webhook endpoints) send no header.
  • You see all of a linked merchant's objects, including those its staff or other integrations created. A merchant can be linked to several partners.

Connect a funeral home already on GracePay

This is the usual path. You create a link request and send a merchant admin to its approval page. The admin signs in, chooses the organization and approves.

CallPurpose
POST /merchant_linksCreate a link request. Body: return_url (required), external_ref, metadata, email (optional, shown as a hint only). Idempotency-Key accepted.
GET /merchant_links/{id}, GET /merchant_linksRead and list. Filter: status.
POST /merchant_links/{id}/cancelCancel a pending request.

Fields: id (mlr_…), status (pending, approved, declined, canceled, expired), url (the approval page, null unless pending), merchant_id, return_url, external_ref, metadata, livemode, created, expires_at (7 days), resolved_at.

  • return_url is an exact https:// URL on one of your registered return origins, which GracePay registers for you. Without one, the call returns 422 return_origin_not_registered.
  • After approval or decline, the browser returns to return_url?link_request=mlr_…&status=…. That return is never the outcome. Read GET /merchant_links/{id} from your server, or handle merchant.linked.
  • status=no_account means the person has no GracePay account. Create the merchant with POST /merchants instead.
  • merchant_id is set while the link the request created is active. After a disconnect it is null again, and the request stays approved.
  • A request created before the merchant's most recent disconnect from you can't link it again. Create a new one.
Create a link request
curl https://api.trygracepay.com/v1/merchant_links \
  -H "Authorization: Bearer $GRACEPAY_PARTNER_KEY" \
  -H "Idempotency-Key: $UNIQUE_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "return_url": "https://cms.example.com/gracepay/return", "external_ref": "tenant-1042" }'

Create a funeral home new to GracePay

POST /merchants (see Merchants) creates the merchant, links it to you and returns onboarding_url for its owner. It can take payments once onboarding and underwriting are complete.

Webhooks for every linked merchant

  • A partner key without GracePay-Merchant manages your own endpoints with the usual /webhook_endpoints and /webhook_deliveries calls. Up to 16 per environment.
  • One endpoint receives the subscribed events of every merchant linked to you. merchant_id on each event and delivery says which merchant.
  • merchant.linked and merchant.unlinked go only to your own endpoints. Merchant endpoints can't subscribe to them (400).
  • If you also have per-merchant endpoints, each subscribed endpoint gets the event once. Deduplicate on evt_.
  • After a disconnect, deliveries of that merchant's events return 404 and can't be retried. Deliveries of merchant.linked and merchant.unlinked stay visible.
merchant.linked
{
  "id": "evt_…",
  "type": "merchant.linked",
  "api_version": "v1",
  "livemode": false,
  "merchant_id": "mer_…",
  "data": {
    "object": {
      "merchant_id": "mer_…",
      "status": "active",
      "source": "approved",
      "started": "2026-11-04T15:04:05Z",
      "ended": null
    }
  }
}

The merchant_link object carries no merchant profile or money data. status: active, ended. source: created (your POST /merchants), approved (a merchant admin approved your request) or operator (GracePay linked it). Act on the current state: GET /merchants/{id} returns 404 once unlinked.

Origins for checkout

  • Your own pages, for example a hosted family site, use partner origins. GracePay registers them after checking that you control the domain. They apply only to checkout sessions your keys create for merchants linked to you.
  • A funeral home's own website, or a subdomain per funeral home, is added for that merchant through /allowed_origins.
  • Exact https:// origins only. Wildcards are refused.

Disconnects

A merchant's admins can disconnect you at any time in Settings › Organization › Developers, and GracePay can too. From then on:

  • Calls for that merchant return 404.
  • Checkout sessions you created can no longer start a payment, and those without a started payment are canceled. A payment already under way completes as the merchant's payment.
  • The allowed origins you added for the merchant are removed, and the per-merchant webhook endpoints your keys created are turned off.
  • You receive merchant.unlinked. Connecting again needs a new link request.

Sandbox

Build against the sandbox with a gp_test_… partner key. It never moves money.

URLSandbox (test keys)Production (live keys)
API base URLhttps://app.stage.trygracepay.com/v1https://api.trygracepay.com/v1
Checkout scripthttps://app.stage.trygracepay.com/v1/checkout.jshttps://app.trygracepay.com/v1/checkout.js
Checkout frame and session urlhttps://app.stage.trygracepay.com/checkout/cs_…https://app.trygracepay.com/checkout/cs_…
  • The script frames checkout from the origin it was loaded from, so a sandbox session is framed from the sandbox origin.
  • Test merchants you create need checkout approval on the payment processor's test account before card or ACH test payments work. GET /capabilities shows card and ach as enabled once they can pay.
  • Where a surface isn't a web page with an https:// origin, such as a native tablet app, open the session's url instead of the frame.

Request access

A merchant's organization admins create merchant keys in the GracePay app at Settings › Organization › Developers. Partner keys, which send GracePay-Merchant, are issued by GracePay on request. Request them here.

Features you're interested in

We'll use these details to respond to your request, as our Privacy Policy describes. API use is subject to our Terms of Service. Please don't include payment information or sensitive family details.