Developers

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 methodWhat the buyer gets
ACCESS_URLA redirect to a page on your own site or app, plus that link by email.
CONTENTA private page we host showing the links and steps you wrote. Good for a document link, a community invite, or a set of instructions.
FILEA private download link for a file you upload to us, up to 25 MB.
CODESOne 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:

EventWhen
sale.completedA one time or subscription purchase is paid.
sale.refundedA purchase is refunded. Revoke access.
subscription.updatedA subscription becomes active, past due, or canceled, or its renewal date changes.
testSent 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 type is sale.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 a CODES product, read data.code.
  • Add a second path for sale.refunded and for subscription.updated with a status that is not active or trialing, 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.completed create or link an account for buyer.email and grant access to product.id. For a subscription, use data.subscription.currentPeriodEnd as the access expiry and extend it on each subscription.updated.
  • To provision from a screen in your app instead, use a CODES product and call the redemption API when the user pastes their key.
  • On sale.refunded, and on subscription.updated with a status that is not active or trialing, revoke.
  • When a buyer arrives without a fresh webhook, ask for their email and call the entitlement API.

Start integrating

Create a business account and open Integrations.