Connect your product
When a buyer pays, Closr needs to tell your system to grant access. Two mechanisms do that: a signed fulfillment webhook we send you, and an entitlement API you can call to verify a buyer. Both use one API key and one signing secret, generated in your dashboard under Integrations.
Do you even need to integrate
Often no. Closr can hand the product to the buyer for you. When you add a product, pick how buyers receive it:
| Delivery method | What the buyer gets |
|---|---|
ACCESS_URL | A redirect to a page on your own site or app, plus that link by email. |
CONTENT | A private page we host showing the links and steps you wrote. Good for a document link, a community invite, or a set of instructions. |
FILE | A private download link for a file you upload to us, up to 25 MB. |
CODES | One license key or coupon from a pool you paste in. Each paid sale claims the next unused key. |
Every buyer also gets an email with a link to https://closr.run/d/<saleId>, and can request it again at https://closr.run/access. You are emailed on every sale. Integrate only when you need to create accounts in your own system, or activate a subscription inside your app. That is what the rest of this page covers.
Get your credentials
Sign in as a business, open Integrations, and:
- Click Generate API key. Copy it once; it is shown only at creation. Rotating it invalidates the old key.
- Set your webhook endpoint URL. Saving it generates a signing secret, shown in full on the page.
- Use Send test event to fire a sample payload at your endpoint, and watch it in the deliveries table.
Fulfillment webhook
We POST JSON to your endpoint on these events:
| Event | When |
|---|---|
sale.completed | A one time or subscription purchase is paid. |
sale.refunded | A purchase is refunded. Revoke access. |
subscription.updated | A subscription becomes active, past due, or canceled, or its renewal date changes. |
test | Sent by the "Send test event" button. |
Payload
{
"id": "evt_9c1e...",
"type": "sale.completed",
"createdAt": "2026-08-28T14:01:33.000Z",
"data": {
"saleId": "021c06a1-7c2b-4227-8afb-79288a223f52",
"product": { "id": "6660...", "name": "NEXUS AI", "billingType": "ONE_TIME", "deliveryType": "CODES" },
"buyer": { "email": "buyer@example.com", "name": "Sam Buyer" },
"quantity": 1,
"amount": 29,
"currency": "USD",
"salesCode": "PVS-125215",
"code": "NEXUS-4KQ2-9F11",
"subscription": null
}
}
Verify the signature
Every request carries x-pvs-signature: sha256=<hex>, an HMAC SHA256 of the raw request body using your signing secret. Also sent: x-pvs-event-id and x-pvs-event-type. Respond with a 2xx within 8 seconds. Non 2xx or a timeout is retried 3 times, then daily for 24 hours; you can also resend from the dashboard.
import crypto from "node:crypto";
app.post("/webhooks/closr", express.raw({ type: "application/json" }), (req, res) => {
const expected = "sha256=" + crypto
.createHmac("sha256", process.env.PVS_WEBHOOK_SECRET)
.update(req.body)
.digest("hex");
const got = req.get("x-pvs-signature") || "";
if (got.length !== expected.length ||
!crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected))) {
return res.status(400).send("bad signature");
}
const event = JSON.parse(req.body.toString());
// grant, revoke, or update access for event.data.buyer.email
res.sendStatus(200);
});
Entitlement API
Check what a buyer is entitled to, by email. Use it to gate access when someone shows up without a fresh webhook, or to reconcile.
curl -H "Authorization: Bearer $PVS_API_KEY" \
"https://closr.run/api/v1/entitlements?email=buyer@example.com"
{
"entitlements": [
{
"type": "one_time",
"productId": "6660...",
"productName": "NEXUS AI",
"status": "active",
"saleId": "021c06a1-...",
"purchasedAt": "2026-08-28T14:01:33.000Z"
},
{
"type": "subscription",
"productId": "7a2b...",
"productName": "Growth Plan",
"status": "active",
"subscriptionId": "sub_1U9...",
"currentPeriodEnd": "2026-09-28T00:00:00.000Z",
"saleId": "8c4d-...",
"purchasedAt": "2026-08-10T09:00:00.000Z"
}
]
}
An empty array means no active entitlement for that email under your account. A missing or wrong key returns 401. The endpoint is rate limited to 120 requests per minute per key.
Redemption API
For a product with the CODES delivery method, the buyer receives a license key. The lowest effort way to connect a SaaS is to add one screen where a new user pastes that key. Your backend calls this endpoint to confirm the key belongs to a real paid sale, then activates the account.
curl -X POST -H "Authorization: Bearer $PVS_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "code": "NEXUS-4KQ2-9F11", "externalUserId": "your-user-42", "externalEmail": "buyer@example.com" }' \
"https://closr.run/api/v1/redemptions/redeem"
{
"productId": "6660...",
"productName": "NEXUS AI",
"buyerEmail": "buyer@example.com",
"billingType": "SUBSCRIPTION",
"saleId": "021c06a1-...",
"saleStatus": "APPROVED",
"subscription": { "status": "active", "currentPeriodEnd": "2026-09-28T00:00:00.000Z", "cancelAtPeriodEnd": false }
}
The first successful call records who redeemed the key. A later call with the same externalUserId returns 200 again, so retries are safe. A call with a different externalUserId returns 409. A key that is not tied to a completed purchase, or whose sale was refunded or cancelled, returns 409. An unknown key returns 404.
To check a key again later, for example when a subscription webhook tells you a plan changed, read its status:
curl -H "Authorization: Bearer $PVS_API_KEY" \
"https://closr.run/api/v1/redemptions/NEXUS-4KQ2-9F11"
{
"valid": true,
"redeemed": true,
"redeemedAt": "2026-08-28T14:05:00.000Z",
"redeemedBy": "your-user-42",
"productId": "6660...",
"productName": "NEXUS AI",
"saleStatus": "APPROVED",
"subscription": { "status": "active", "currentPeriodEnd": "2026-09-28T00:00:00.000Z", "cancelAtPeriodEnd": false }
}
No code, with Zapier or Make
If you cannot add webhook code to your product, point the fulfillment webhook at a catch hook in Zapier or Make and build the rest with their steps, no code required.
- In Zapier, create a Zap with the Webhooks by Zapier trigger, Catch Hook. Copy its URL into Integrations, webhook endpoint URL, then press Send test event so Zapier sees the shape.
- Add a Filter step so the Zap continues only when
typeissale.completed. - Add your action: create the user in your tool, send an invite, add a row to a sheet, or enroll the buyer in a course platform using
data.buyer.email. For aCODESproduct, readdata.code. - Add a second path for
sale.refundedand forsubscription.updatedwith a status that is notactiveortrialing, and have it remove access.
Signature checking is optional on a private catch hook URL. If you want it, Zapier and Make can compute an HMAC SHA256 in a code step and compare it to x-pvs-signature.
Recommended flow
- If the platform can deliver the product for you, use a delivery method and skip the rest.
- To provision in your own system automatically, on
sale.completedcreate or link an account forbuyer.emailand grant access toproduct.id. For a subscription, usedata.subscription.currentPeriodEndas the access expiry and extend it on eachsubscription.updated. - To provision from a screen in your app instead, use a
CODESproduct and call the redemption API when the user pastes their key. - On
sale.refunded, and onsubscription.updatedwith a status that is notactiveortrialing, revoke. - When a buyer arrives without a fresh webhook, ask for their email and call the entitlement API.