SimplePay integration context
SimplePay public API:
- Base URL: https://api.simplepay.mx
- Auth: Authorization: Bearer ${SIMPLEPAY_API_KEY}
- Sandbox keys look like spk_sandbox_...
- Hosted checkout URLs are on https://checkout.simplepay.mx
- API contract: /openapi.json
Security rules:
- Never expose API keys in browser/mobile code.
- Never commit, print or persist raw secrets.
- Use only documented public SimplePay endpoints.
- Do not put secrets, payment credentials or raw payment data in metadata.
- Use sandbox first.
Idempotency:
- Include Idempotency-Key on writes.
- Use deterministic keys from business IDs.
- Examples: order_123_checkout_v1, booking_456_refund_v1.
- Retry 5xx/429 only with the same idempotency key.
Hosted Checkout:
- Endpoint: POST /v1/hosted-checkout/sessions
- Required body: currency, success_url, line_items.
- line_items use amount in minor units.
- Store response id spcs_... and url.
- Redirect user to session.url.
- success_url is UX only, not proof of payment.
- Confirm final state by webhook or API read.
Payment Links:
- Endpoint: POST /v1/payment-links
- Use for invoices, async sales, manual collection.
- Store splink_... and URL.
- Use max_payments: 1 for one-off invoices.
Refunds:
- Endpoint: POST /v1/refunds
- Use SimplePay-managed payment_intent references.
- Use Idempotency-Key.
- For Bookable refunds, prefer POST /v1/bookable-payments/bookings/{id}/refund.
Bookable Payments:
- Product for reserved inventory where payment confirms booking.
- Merchant owns login, discovery and calendar UI.
- SimplePay owns availability math, temporary holds, checkout and booking confirmation.
- Core objects: product, booking_resource, availability_schedule, availability_rule, availability_override, booking_blackout, booking_event_type, booking_hold, booking.
- Shortcut setup: POST /v1/bookable-payments/setup.
- Slots: GET /v1/bookable-payments/event_types/{id_or_slug}/slots.
- Atomic checkout: POST /v1/bookable-payments/bookings.
- Split flow: POST /v1/bookable-payments/holds then POST /v1/bookable-payments/holds/{id}/payment.
- Booking remains pending until payment webhook projection confirms it.
- Deliver voucher/access only after booking.confirmed or GET booking returns status=confirmed.
Webhook endpoints:
- Configure with POST /v1/webhook_endpoints.
- Payload shape: { "id": "...", "type": "...", "data": { ... } }
- Header: SimplePay-Signature: t=<unix>,v1=<hex hmac>
- Signature payload: `${timestamp}.${rawBody}`
- Algorithm: HMAC SHA-256 with webhook signing secret.
- Verify raw body before JSON parsing side effects.
- Deduplicate by event.id.
- Respond 2xx after durable persistence.
- Replay with POST /v1/events/{id}/replay.
Important Bookable events:
- booking.hold_created
- booking.hold_released
- booking.confirmed
- booking.expired
- booking.cancelled
- booking.rescheduled
- booking.refunded
- booking.partially_refunded
- booking.no_show
- product.updated
- customer.updated
- booking_event_type.updated
Implementation deliverables:
- Backend endpoint to create checkout or booking checkout.
- Env vars and secret handling.
- Persistence of SimplePay IDs.
- Webhook handler with HMAC verification and idempotency.
- Tests for signature verification, duplicate events and retry/idempotency.
- Sandbox smoke instructions.
Minimal checkout request
request
{
"currency": "mxn",
"success_url": "https://example.com/success?session_id={CHECKOUT_SESSION_ID}",
"cancel_url": "https://example.com/cart",
"client_reference_id": "order_1001",
"line_items": [
{
"name": "Producto demo",
"amount": 50000,
"quantity": 1
}
],
"metadata": {
"order_id": "order_1001"
}
}
Minimal webhook verification
verify-simplepay.ts
import crypto from "node:crypto";
export function verifySimplePaySignature(
rawBody: string,
header: string,
secret: string,
) {
const values = Object.fromEntries(
header.split(",").map((part) => {
const [key, value] = part.split("=");
return [key, value];
}),
);
const timestamp = Number(values.t);
const signature = values.v1;
if (!timestamp || !signature) {
throw new Error("Invalid SimplePay-Signature");
}
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
if (
!crypto.timingSafeEqual(
Buffer.from(signature, "hex"),
Buffer.from(expected, "hex"),
)
) {
throw new Error("Invalid SimplePay signature");
}
}
Para shapes exactos, no adivines. Lee
/openapi.json y genera tipos desde
OpenAPI.