How Gumroad payments flow
Gumroad integration is one-way: Gumroad pushes sale events to Keylight, Keylight mints a license, you email the key. There is no OAuth, no Gumroad API call from Keylight, and no outbound license.created webhook (unlike Stripe Connect) — the response from the ping is your only chance to capture the raw key.
The path
Section titled “The path”Step 1 — customer pays on Gumroad
Section titled “Step 1 — customer pays on Gumroad”Standard Gumroad checkout. Nothing Keylight-specific happens here. On success, Gumroad redirects the customer to Gumroad’s “Thanks for your purchase” page (or your custom thank-you redirect, if configured) and queues a ping for the sale.
Step 2 — Gumroad fires the ping
Section titled “Step 2 — Gumroad fires the ping”Gumroad posts an application/x-www-form-urlencoded body to your configured ping URL:
POST /webhooks/gumroad/<tenantId>?token=<shared-secret>Key fields in the form body include resource_name, sale_id, email, full_name, product_name, price, currency, and product_permalink. The full field list and what Keylight does with each is in the Webhook reference.
Gumroad retries pings on non-2xx responses. Keylight is idempotent on sale_id (see below).
Step 3 — Keylight mints the license
Section titled “Step 3 — Keylight mints the license”Keylight runs through, in order:
- Auth. Token query param compared against
tenant.gumroadWebhookToken. - Lifecycle check. Tenant must be in a state that allows new license creation.
- Event filter. Skip anything that isn’t a real sale (test pings, refunds, etc. →
204). - Idempotency. Read
gumroad:issued:<tenantId>:<sale_id>from KV. If present, return200 {"received":true,"duplicate":true}. - Permalink resolution. Look up
tenant.gumroadProductMap[product_permalink]→ KeylightproductId. Reject400on miss. - Issue license. Generate the key, store hash + masked form, write
LicenseRecordto Postgres + Durable Object. - Write payment record.
PaymentRecordin Postgres withsource: 'gumroad'. Best-effort — failure is logged but does not fail the response, so Gumroad doesn’t retry and risk a double-issue.
The HTTP response carries the raw license key in the body — once, and only once.
Step 4 — customer activates
Section titled “Step 4 — customer activates”The customer launches your app, pastes the key, and the SDK calls /activate. First activation consumes one slot; subsequent launches re-use the cached Ed25519 lease. See How it works for the SDK side of the handshake.
Key-type behavior
Section titled “Key-type behavior”Gumroad’s payload doesn’t carry a key_type_id. Every Gumroad sale gets the product’s first key type — its activation limit and expiry. If you need to sell multiple key types (e.g. “personal” and “team”) via Gumroad, model them as separate Gumroad products and map each permalink to a different Keylight productId.
For per-key-type pricing within a single product, use Stripe Connect — that flow does pass key_type_id through checkout-session metadata.
What can go wrong
Section titled “What can go wrong”| Symptom | Likely cause |
|---|---|
400 Invalid request on every ping | Token in URL doesn’t match tenant.gumroadWebhookToken (rotated? typo?) |
400 Missing required fields | Gumroad payload missing sale_id or email (only seen on hand-rolled retries) |
400 No product mapping for permalink '...' | Permalink isn’t in Settings → Integrations → Gumroad |
403 | Tenant lifecycle state forbids new licenses |
| Customer paid but no email arrived | Your ping forwarder didn’t capture the response, or your sender is down — re-issue from the dashboard |
| Duplicate licenses | Should be impossible due to sale_id idempotency; if seen, file a bug with the sale_id |
Related
Section titled “Related”- Gumroad setup — enable the integration and map permalinks.
- Webhook reference — field-by-field detail and status codes.
- Stripe payment flow — same shape, with Stripe and outbound license webhooks.