API
Moonlit Pay is the billing layer for Moonlit and partner apps. A customer is an email address. Your server creates a checkout, sends the customer there, and hears back through signed webhooks.
Authentication
Every app has secret keys, created in the dashboard under Developers. Send one as a bearer token from your server. Never expose it in the browser.
Authorization: Bearer mp_sk_... Base URL: https://pay.moonlit.company/api/v1
Errors return a non-2xx status and { "error": { "code", "message" } }.
Checkout
Create a checkout for one of the app's plans and redirect the customer to the returned URL. Passing email skips the email step. externalId is your own user id and comes back on webhooks.
curl -X POST https://pay.moonlit.company/api/v1/checkout \
-H "Authorization: Bearer $MOONLIT_PAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"plan": "pro-monthly",
"email": "ava@example.com",
"externalId": "user_1042",
"successUrl": "https://app.example.com/billing?paid=1",
"cancelUrl": "https://app.example.com/pricing"
}'
// 201
{ "id": "9f2c...", "url": "https://pay.moonlit.company/checkout/9f2c..." }successUrl falls back to the app's return URL. Links expire after 24 hours.
Billing portal
Returns a link to the customer's portal with this app's subscription at the top. The customer verifies their email there, and sees subscriptions across every Moonlit and partner app.
curl -X POST https://pay.moonlit.company/api/v1/portal \
-H "Authorization: Bearer $MOONLIT_PAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "email": "ava@example.com" }'
{ "url": "https://pay.moonlit.company/portal?app=doclet-cloud&email=ava%40example.com" }Subscriptions
Check what a customer has for this app. entitled is true for active, trialing and past due subscriptions.
GET https://pay.moonlit.company/api/v1/subscriptions?email=ava@example.com
{
"email": "ava@example.com",
"active": true,
"subscriptions": [{
"id": "k57...", "plan": "pro-monthly", "planName": "Pro",
"status": "active", "entitled": true,
"cancelAtPeriodEnd": false, "currentPeriodEnd": 1767225600,
"externalId": "user_1042"
}]
}Plans
GET https://pay.moonlit.company/api/v1/plans
{ "plans": [{ "key": "pro-monthly", "name": "Pro", "type": "subscription",
"amount": 1900, "currency": "usd", "interval": "month", "trialDays": 7 }] }Payments
GET https://pay.moonlit.company/api/v1/payments?email=ava@example.com
{ "payments": [{ "id": "in_...", "plan": "pro-monthly", "kind": "subscription",
"status": "succeeded", "amount": 1900, "currency": "usd",
"description": "Pro", "createdAt": 1764633600 }] }Webhooks
Add endpoints in the dashboard. Each delivery is a JSON POST signed with your endpoint's secret. Failed deliveries retry for about two and a half hours.
Moonlit-Signature: t=1764633600,v1=5257a869e7ec...
{
"id": "evt_9a1c...",
"type": "subscription.created",
"created": 1764633600,
"app": "doclet-cloud",
"data": {
"customer": { "email": "ava@example.com" },
"subscription": { "id": "k57...", "status": "active", "plan": "pro-monthly",
"amount": 1900, "currency": "usd", "interval": "month",
"currentPeriodEnd": 1767225600, "cancelAtPeriodEnd": false,
"externalId": "user_1042" }
}
}Verify by computing HMAC-SHA256 of `${t}.${rawBody}` with the secret and comparing it to v1 in constant time. Reject timestamps older than five minutes and deduplicate on id.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(rawBody: string, header: string, secret: string) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const a = Buffer.from(expected), b = Buffer.from(parts.v1 ?? "");
return a.length === b.length && timingSafeEqual(a, b);
}checkout.completedA checkout was paidsubscription.createdA subscription became active or started its trialsubscription.updatedStatus, renewal date, price or cancellation changedsubscription.canceledA subscription endedpayment.succeededA one-time or recurring payment went throughpayment.failedA charge failedpayment.refundedA payment was fully refunded