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
- Server:
POST /invoiceswithissue: true. - Server:
POST /checkout_sessions, then passclient_tokento the browser. - Browser: render
<gracepay-checkout>. - Webhook:
payment.completedrecords the payment.invoice.paidcloses the invoice whenpaid_minorreachestotal_minor.
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 }
]
}'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"
}'<script type="module" src="https://app.trygracepay.com/v1/checkout.js"></script>
<gracepay-checkout client-token="cst_…"></gracepay-checkout>{ "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
- Record money expected from elsewhere with
POST /other_payments. - Mark it received or canceled as it resolves.
- Send whatever stays open through a checkout session.
Conventions
| Item | Contract |
|---|---|
| Base URL | https://api.trygracepay.com/v1 for live keys. Test keys use the staging sandbox at https://app.stage.trygracepay.com/v1 (see Partner platforms › Sandbox). |
| Environments | Selected 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. |
| Authentication | Authorization: Bearer <secret key>, server-side only. |
| Acting for a merchant | Partner 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. |
| Idempotency | Send 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. |
| Money | Integer minor units in *_minor fields, with currency (USD). null means not yet reported, never zero. |
| Time | RFC 3339 UTC. Calendar dates (due_date, date_of_death) are YYYY-MM-DD. |
| IDs | Opaque, prefixed strings. Do not parse them. Prefixes: mer_, loc_, inv_, cs_, pay_, re_, op_, we_, dlv_, evt_, req_. |
| Lists | limit (1-100, default 25), starting_after, returning { "data": [], "has_more": false }. Newest first. |
| Additive changes | New fields, statuses and event types can appear within v1. Clients must tolerate unknown values. |
| Rate limits | 429 with Retry-After. |
Keys and settings
Organization admins manage these in the GracePay app at Settings › Organization › Developers.
| Setting | What it does |
|---|---|
| API keys | Create, name and revoke merchant keys. The secret is shown once. |
| Allowed host names | The 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 platforms | The partners linked to the organization, with how and when they were linked. Admins can disconnect one at any time. |
| Webhook endpoints | The 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": {
"type": "invalid_request",
"code": "amount_exceeds_balance",
"message": "…",
"param": "amount_minor",
"request_id": "req_…"
}
}| Status | type | Meaning |
|---|---|---|
| 400, 422 | invalid_request | The request is malformed or breaks a rule. |
| 401 | authentication | Missing or invalid key. |
| 403 | permission | Includes capability_not_enabled. |
| 404 | not_found | Missing, or not visible to this key. |
| 409 | conflict | Includes idempotency_conflict and invalid_state. |
| 429 | rate_limited | Slow down. |
| 5xx | api_error | The 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.
{
"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.
| Call | Purpose |
|---|---|
POST /merchants | Create a merchant. Returns the merchant with onboarding_url. |
GET /merchants/{id}, GET /merchants | Read and list. |
GET /merchants/{id}/locations | List locations. |
Request body
| Field | Description |
|---|---|
legal_name | The merchant's legal name. |
email | The merchant's email address. |
external_ref | Optional. Your reference. |
metadata | Optional. 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.
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
| Call | Purpose |
|---|---|
POST /invoices | Create, and optionally issue with issue: true. |
GET /invoices/{id}, GET /invoices | Read and list. Filters: status, location_id, external_ref. |
POST /invoices/{id}/issue | Issue a draft. Issued lines are immutable. |
POST /invoices/{id}/void | Void. Refused while money is applied or reserved, or while any other payment on the invoice is expected or received. |
Request body
| Field | Description |
|---|---|
location_id | The merchant location. |
currency | USD. |
due_date | When the invoice is due. |
memo | A note on the invoice. |
external_ref | Your reference. Can be used as a list filter. |
metadata | Your data. |
payer | { name, email, phone }. |
decedent | Optional. { name, date_of_death }. |
lines[] | { description, category, amount_minor, taxable }. 1-100 lines. |
issue | true 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.
{
"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
| Call | Purpose |
|---|---|
POST /checkout_sessions | Create. Reserves the amount on the invoice. |
GET /checkout_sessions/{id} | Read. |
POST /checkout_sessions/{id}/cancel | Cancel. Releases the reservation only once the session is verified closed without a payment. |
Request body
| Field | Description |
|---|---|
invoice_id | The invoice to collect on. |
amount_minor | Default: 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_url | Optional. |
expires_in | Seconds, 300-86400. Default 3600. |
metadata | Your data. |
Response fields
| Field | Description |
|---|---|
id | The session. |
status | open, processing, completed, expired, canceled. |
client_token | Browser-safe. Passed to the component. Only while the session is open or processing; null once it is closed and in webhook event snapshots. |
url | The hosted page. Like client_token, only while the session is open or processing. |
expires_at | When the session expires. |
payment_id | The resulting payment. |
{
"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
| Status | Meaning |
|---|---|
pending | The payment has not finished. |
completed | The payment succeeded. |
finalized | It settled at the processor. This does not mean a bank deposit arrived. |
canceled | The payment was canceled. |
{
"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
| Call | Purpose |
|---|---|
POST /payments/{id}/refunds | Body: amount_minor (default: remaining), reason. Idempotency-Key required. |
GET /refunds/{id}, GET /refunds | Read 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
requestedfor 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
requestedwhile 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.
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.
| Call | Purpose |
|---|---|
POST /other_payments | Record money expected or received. Idempotency-Key required. |
GET /other_payments/{id}, GET /other_payments | Read and list. Filters: invoice_id, status, external_ref. |
POST /other_payments/{id}/receive | Mark it received: expected to received. |
POST /other_payments/{id}/cancel | Cancel it: expected to canceled. |
Request body
| Field | Description |
|---|---|
invoice_id | The invoice the money is for. |
amount_minor | Greater than zero. Not more than the invoice's open balance. |
currency | USD. |
status | expected (default) or received. |
expected_date | Optional. YYYY-MM-DD. |
source | Optional. A label of up to 100 characters, shown to the merchant's staff. |
reference | Optional. Up to 100 characters. |
description | Optional. Up to 500 characters. |
external_ref | Your reference. Can be used as a list filter. |
metadata | Your data. |
Receive and cancel
| Call | Body |
|---|---|
POST /other_payments/{id}/receive | amount_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}/cancel | reason (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
| Status | Meaning |
|---|---|
expected | Counts toward the invoice's covered_minor. The payer is not asked for that amount. |
received | Counts toward paid_minor. |
canceled | The amount returns to the invoice balance. |
reversed | The 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
receivedother payment cannot be canceled through the API. It returns409 invalid_state. - A partial reversal keeps
received.reversed_minorshows how much was reversed, and only the rest counts towardpaid_minor. invoice.paidfires, and the invoice becomespaid, only whenpaid_minorreachestotal_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
expectedorreceived.
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"
}'{
"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"
}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
| Call | Purpose |
|---|---|
POST /webhook_endpoints | Body: url (HTTPS), events[], description. The response contains secret (whsec_…), shown once. |
GET, PATCH, DELETE /webhook_endpoints/{id}, GET /webhook_endpoints | Manage. PATCH takes url, events[], description, enabled. |
POST /webhook_endpoints/{id}/rotate_secret | New 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}/retry | Send 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.
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.
| Call | Purpose |
|---|---|
GET /allowed_origins | List the merchant's origins. |
POST /allowed_origins | Body: 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.
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
POST /your/endpoint
Content-Type: application/json
GracePay-Signature: t=1793491200,v1=5f8a…
GracePay-Delivery: dlv_…{
"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.
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);
});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 "", 200Delivery
- Any
2xxwithin 10 seconds acknowledges. - Otherwise deliveries retry with exponential backoff, at most one hour apart, for 7 days.
idis stable across retries, so deduplicate on it.- Order is not guaranteed, and a delayed event carries an older snapshot.
GETthe object and act on its current state, never on the event's. A payment counts as paid when its current status iscompletedorfinalized.
Events
| Events | When |
|---|---|
merchant.status_changed | Onboarding or standing changed. |
invoice.issued, invoice.updated, invoice.paid, invoice.voided | Invoice lifecycle. |
checkout_session.completed, checkout_session.expired, checkout_session.canceled | Session lifecycle. |
payment.pending, payment.completed, payment.finalized, payment.canceled | Payment lifecycle. payment.completed is the fulfilment signal. |
refund.received, refund.completed, refund.failed, refund.reversed | Refund lifecycle. |
other_payment.created, other_payment.received, other_payment.canceled, other_payment.reversed | Other payment lifecycle. |
merchant.linked, merchant.unlinked | A merchant was linked to or disconnected from your partner account. Partner endpoints only. |
Checkout component
<script type="module" src="https://app.trygracepay.com/v1/checkout.js"></script>
<gracepay-checkout client-token="cst_…"></gracepay-checkout>Attributes
| Attribute | Required | Purpose |
|---|---|---|
client-token | Yes | client_token from the checkout session. Single session, short-lived. Environment comes from the token. |
appearance | No | light (default), dark. |
suppress-autoexpand | No | Do not auto-open the method when only one is offered. |
Events
All events bubble and are composed.
| Event | detail | Meaning |
|---|---|---|
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.jsand it frameshttps://app.trygracepay.com. Oncehttps://js.trygracepay.comis published, the script from there frameshttps://checkout.trygracepay.com. In the sandbox, usehttps://app.stage.trygracepay.comfor both. - A host page with a Content Security Policy adds the script's origin to
script-srcand the frame's origin toframe-src. - The page's exact origin must be in the session's
allowed_originsand 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'surlinstead of the frame.
Apple Pay and PCI
- Apple Pay is not offered inside the frame. Open the session's
urlas 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.
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 return404. - 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.
| Call | Purpose |
|---|---|
POST /merchant_links | Create 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_links | Read and list. Filter: status. |
POST /merchant_links/{id}/cancel | Cancel 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_urlis an exacthttps://URL on one of your registered return origins, which GracePay registers for you. Without one, the call returns422 return_origin_not_registered.- After approval or decline, the browser returns to
return_url?link_request=mlr_…&status=…. That return is never the outcome. ReadGET /merchant_links/{id}from your server, or handlemerchant.linked. status=no_accountmeans the person has no GracePay account. Create the merchant withPOST /merchantsinstead.merchant_idis set while the link the request created is active. After a disconnect it isnullagain, and the request staysapproved.- A request created before the merchant's most recent disconnect from you can't link it again. Create a new one.
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-Merchantmanages your own endpoints with the usual/webhook_endpointsand/webhook_deliveriescalls. Up to 16 per environment. - One endpoint receives the subscribed events of every merchant linked to you.
merchant_idon each event and delivery says which merchant. merchant.linkedandmerchant.unlinkedgo 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
404and can't be retried. Deliveries ofmerchant.linkedandmerchant.unlinkedstay visible.
{
"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.
| URL | Sandbox (test keys) | Production (live keys) |
|---|---|---|
| API base URL | https://app.stage.trygracepay.com/v1 | https://api.trygracepay.com/v1 |
| Checkout script | https://app.stage.trygracepay.com/v1/checkout.js | https://app.trygracepay.com/v1/checkout.js |
Checkout frame and session url | https://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 /capabilitiesshowscardandachasenabledonce they can pay. - Where a surface isn't a web page with an
https://origin, such as a native tablet app, open the session'surlinstead 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.