Payments
Webhooks
How Keyrail receives, verifies and processes payment provider webhooks, and what it answers.
Keyrail receives webhooks from your payment provider and turns them into license changes. You don’t write any webhook code: you paste one URL into your provider.
Endpoint#
POST https://api.keyrail.tech/v1/webhooks/{provider}/{integrationId}provider is paddle or lemonsqueezy; integrationId is the int_… ID of your integration. The full URL is returned as webhookUrl when you create the integration.
Signature verification#
Every request is verified with the integration’s webhookSecret before anything else. An invalid or missing signature gets 401 and is not processed.
| Provider | Header | Check |
|---|---|---|
| Paddle Billing | Paddle-Signature: ts=…;h1=… | h1 = HMAC-SHA256 of ts:rawBody; ts within 5 minutes. |
| Lemon Squeezy | X-Signature | Hex HMAC-SHA256 of the raw body. |
Responses#
| Situation | Response | Provider behavior |
|---|---|---|
| Processed | 200 | Done. |
| Event already processed (same provider event ID) | 200 { "result": "duplicate" } | Done. Retries are safe. |
Price or variant not in priceMap | 200 { "result": "ignored" }, and an event in the audit log | Done. Map the price, then replay the event from your provider if needed. |
| Bad signature, or no secret saved yet | 401 | Check the secret. |
| No integration with this ID (typo, or disconnected) | 404 | Copy the Webhook URL again. |
| Hourly budget of license-key emails of your Keyrail plan used up | 429 | The provider retries later. |
| Another key e-mail for the same license is in progress | 409 | Retry later; this event has not been marked processed. |
| Processing error | 500 | The provider retries later. |
Processing rules#
- Customer: matched by email within your account, or created. Paddle events carry a
customer_idinstead of an email: Keyrail maps it fromcustomer.created/customer.updated, or fetches it with the integration’s optional Paddle API key. Until the customer is known, a Paddle transaction gets500and is retried by Paddle. - Plan: looked up in the integration’s
priceMap. A mapped subscription payment or update changes an existing license's plan in place: its key and activated machines stay the same. New entitlements, the default activation limit and the new plan's major-version limit apply at the next certificate refresh; an explicit per-license activation override is kept. A status update without a mapped price keeps the current plan. - One license per customer and plan subscription: re-subscribing after expiry reuses the customer’s license (and key) for the same plan.
- New license: the key is emailed to the customer, branded with your product name and sender name.
- Ordering: subscription status events older than the license’s last processed event are ignored, including concurrent deliveries and events within the same second. An older payment may extend the paid period but cannot restore an older plan, quantity or status. Revocation is final, even when a payment arrives at the same time.
- Concurrent e-mails: only one key send per license runs at a time. Failed sends release their reservation; an abandoned reservation expires after one minute. A provider timeout after it accepted a message can still leave delivery uncertain, so this is not an exactly-once delivery guarantee.
The event-by-event mapping is in each provider guide: Paddle Billing, Lemon Squeezy.
Debugging#
Every webhook received appears in Events and in the History of its license in the dashboard, and through GET /v1/events?license=lic_…. Ignored events (unknown price) are logged too, so a missing mapping is easy to spot. Common cases: troubleshooting.
Need help? See Troubleshooting or write to support@keyrail.tech.