Securing checkout webhooks: API key and signature in Make, Zapier and n8n
Updated
You secure every checkout webhook from Rechnungskit with an API key of your choice, sent as X-Api-Key and also as x-make-apikey. If you want, an HMAC-SHA256 signature in the header X-Rechnungskit-Signature comes on top. In Make this switches on the built-in API key authentication, in n8n the Header Auth of the webhook node, and in Zapier you compare the header with a filter behind a Catch Raw Hook.
With the Rechnungskit checkout you sell through a link, and the invoice is created automatically. What happens after the purchase is up to a webhook: unlock access, generate a license, trigger shipping, process further in Make, Zapier or n8n. This page shows how to set up your webhook receiver so it accepts only genuine messages from Rechnungskit.
- POST with a JSON bodyto your URL
- Headersevent ID, plus API key and signature if set up
What Rechnungskit sends
For every checkout outcome, Rechnungskit sends a POST request with a JSON body to your URL: as checkout.paid on success and as checkout.failed on failure. The message carries these headers (signature and API key only if you set them up when creating the webhook):
| Header | Content |
|---|---|
X-Rechnungskit-Signature |
sha256=<hex>, the HMAC-SHA256 signature of the body, if switched on |
X-Rechnungskit-Event-Id |
Unique event ID for duplicate detection (also in the body) |
X-Api-Key |
Your optional API key, if set when creating the webhook |
x-make-apikey |
The same API key again, in the header name Make checks natively |
You choose which events an endpoint receives when you create it (and any time later) with the event checklist on the webhook. The test button on the webhook checks the whole route per event with a sample event, without triggering a real purchase.
Delivery is at least once. If your receiver does not answer with a 2xx status, Rechnungskit tries again, up to six times in total, with growing intervals.
Subscription events (subscription.*)
If you also sell subscriptions through the checkout, Rechnungskit reports every step in the subscription lifecycle as its own event. That way your system can switch access automatically:
| Event | When it fires | Your typical reaction |
|---|---|---|
subscription.activated |
The subscription is active (first payment confirmed or trial started) | Unlock access |
subscription.charged |
A charge succeeded (start, renewal or upgrade, see charge.kind) |
Extend the term, file the document |
subscription.payment_failed |
A charge failed; charge.attempt/charge.maxAttempts/charge.nextAttemptAt show the retry ladder |
Do NOT lock yet, only at the end of the ladder |
subscription.updated |
Plan or quantity changed; change carries before/after |
Adjust seats/features |
subscription.canceled |
A cancellation came in, effective at the end of the period | Make a note, don't lock anything yet |
subscription.ended |
The subscription is really over (endedReason: period_end, immediate or dunning) |
Lock access now |
subscription.withdrawn |
Consumer withdrawal through the withdrawal button (§ 356a BGB) | Lock access, the refund is under way |
- subscription.activatedunlock access
- subscription.payment_faileddon't block yetonly at the end of the ladder
- subscription.canceledmake a notetakes effect at period end
- subscription.endedrevoke access nowsame for subscription.withdrawn
The subscription.* messages use their own versioned envelope ("version": 1). Besides event, eventId and occurredAt, subscription always holds the complete current state: status, product, plan, quantity, amount, period, buyer, your metadata and booked add-ons as an addons list. change (on changes) and charge (on charges) come as separate blocks.
You recognize test purchases by the test field: for checkout.* it sits at the end of the payload, for subscription.* at the top level of the envelope. "test": true means the purchase ran in test mode or through your test credentials, and no real money moved. For real purchases it says false.
Rely on the absolute state, not on the order of the messages. Delivery is at least once and without an ordering guarantee. Detect duplicates by eventId, and for subscription.updated take the state with the latest occurredAt.
The security is identical to the checkout events: same endpoint, same API key, same signature. One endpoint can subscribe to checkout and subscription events at the same time.
Ad attribution in the payload (click IDs and UTM)
If a buyer lands on the checkout with ad parameters (fbclid, gclid, msclkid, ttclid or utm_source to utm_term) and agrees in the consent bar, Rechnungskit freezes them on the purchase. They then appear in every webhook message:
| Where | Field |
|---|---|
checkout.* |
additional field attribution at the end of the payload |
subscription.* |
attribution inside subscription |
| Redirect to the thank-you page (return URL on the product) | the same parameters plus rk_order, rk_event_id, rk_amount_cents, rk_currency |
The field is null if the buyer came without ad parameters, rejected, or did not decide at all. Without consent, the return URL only gets the rk_ parameters, no click IDs and no UTM parameters.
When the bar appears
The bar only appears when a tracking integration is active in the project (Meta Ads, Google tag or TikTok pixel) and a privacy policy URL is set. Without it the field is always null, because click IDs and UTM parameters are only stored with consent (§ 25 Abs. 1 TDDDG, EDPB Guidelines 2/2023, para. 50 f.).
Rechnungskit checks the stored consent at the time of sending. Orders from before the switch (October 2026) have no record and come without attribution.
What you can use the data for
This lets you report conversions to ad platforms server-side, for example to the Meta Conversions API with fbclid to attribute the campaign. Or you analyze your channels yourself.
The parameters on the return URL are there so your own conversion tag can fire on your thank-you page. rk_event_id is the event ID the native Meta integration also uses, so Meta can deduplicate against your own browser pixel.
The existing field order before the new field stays unchanged, so existing signature checks keep working.
Two safeguards: API key and signature
Your webhook URL is already the first safeguard: it contains a long random part and is practically impossible to guess. If you want more, there are two levels:
- API key (simple): when creating the webhook in Rechnungskit, you enter a key of your choice. It is sent as a header with every delivery. Your receiver only compares the value, no cryptography involved. This is the right way for Make, Zapier and n8n.
- HMAC signature (strong): If you switch on "Also send an HMAC signature" when creating the webhook, Rechnungskit signs the exact JSON body with a signing secret (
whsec_…, shown exactly once after saving). Recomputing the signature proves that the message comes from Rechnungskit and was not altered. This is the right way for your own code and for receivers with crypto support.
The three tools at a glance
| Tool | Where you check the key | Header | Without the matching key |
|---|---|---|---|
| Make | Custom webhook, API Key authentication | x-make-apikey |
HTTP 401 |
| n8n | Webhook node, Authentication Header Auth | X-Api-Key |
HTTP 403 |
| Zapier | Catch Raw Hook plus Filter by Zapier | X_Api_Key (Zapier uses underscores) |
the Zap stops |
Make: API key authentication
Make checks API keys natively on the custom webhook and expects the key in the header x-make-apikey. That is exactly where Rechnungskit sends your key automatically, so no renaming or extra logic is needed.
- In your scenario, add the module Webhooks, Custom webhook (or open your existing webhook) and open the webhook's advanced settings.
- Under API Key authentication, click Add API key and create a new keychain. Enter exactly the key you set as the API key when creating the webhook in Rechnungskit. For security reasons, Make does not show the value again afterwards.
- Save. From now on, Make answers every message without the matching key with HTTP 401, and your scenario only starts for genuine Rechnungskit events.
Two useful extras in the same webhook settings:
- IP restrictions: a comma-separated list of allowed sender IPs.
- Get request headers: if you want to reuse header values in the scenario.
You can also recompute the HMAC signature in Make: the function sha256 accepts a key parameter and then returns an HMAC. That needs JSON pass-through and header capture switched on, though, and a filter that compares the values. For Make we recommend the API key.
n8n: Header Auth
The n8n webhook node has header checking built in as its own authentication type:
- Open your Webhook node and set Authentication to Header Auth.
- Under Credential for Header Auth, create a new credential of type Header Auth. As Name, enter
X-Api-Key, as Value the API key you set in Rechnungskit. The case of the header name does not matter. - Save and activate the workflow. n8n rejects messages without the matching key with HTTP 403.
If you want to check the signature instead, switch on the option Raw Body on the webhook node so the exact body is preserved. Then either the Crypto node (Action Hmac, Type SHA256, Encoding HEX, Secret = your whsec_…) or a Code node with require('crypto') computes the signature. An IF node then compares "sha256=" + hmac with the header x-rechnungskit-signature.
Zapier: Catch Raw Hook and filter
Zapier has no built-in key check on the webhook trigger; the unguessable catch URL is the standard safeguard there. If you also want to check the API key, you need the trigger type with headers:
- As the trigger, use Webhooks by Zapier with the event Catch Raw Hook. Not Catch Hook: the normal Catch Hook only parses the body and returns no headers. Catch Raw Hook requires a paid Zapier plan.
- Fire a test event ("Send test event" in Rechnungskit) so Zapier learns the structure. The headers appear with underscores, so your key is in the field
headers X_Api_Key. - Right after the trigger, add a Filter by Zapier: Only continue if, field
X_Api_Key, condition (Text) Exactly matches, value = your API key.
You can check the signature in Zapier too. The Catch Raw Hook delivers the unchanged body in the field raw. A Code by Zapier step (Python with hmac/hashlib or JavaScript with crypto) computes the signature from it and outputs a comparison result for the filter to check. For most Zaps, the API key filter is the practical way.
Checking the signature yourself (HMAC-SHA256)
For your own endpoints (your server, a cloud function, the n8n code node), you recompute the signature over the unchanged request body, with the signing secret from the webhook setup:
const crypto = require('crypto');
function verify(rawBody, signatureHeader, secret) {
const expected =
'sha256=' + crypto.createHmac('sha256', secret).update(rawBody, 'utf8').digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));
}
Three pitfalls from practice:
- Raw body: you really have to hash the raw body, not re-serialized JSON. Even a different field order changes the signature.
- Safe comparison:
timingSafeEqualis better than a plain===. - Duplicates: detecting them by event ID pays off. Rechnungskit delivers at least once, and on retries the same
X-Rechnungskit-Event-Idcomes again.
Frequently asked questions
Do I need the API key and the signature at the same time?
No. The API key is the simple way for no-code tools and fully sufficient for most cases, especially since your webhook URL cannot be guessed anyway. The signature is the cryptographically stronger proof that the message really comes from Rechnungskit and was not altered on the way.
What gets sent is what you set up when creating the webhook: the API key if you entered one, and the signature if "Also send an HMAC signature" is on. Which of them you check is up to your receiver.
Where do I set the API key in Rechnungskit?
When creating the webhook under Connections → API, in the section Checkout webhooks, field "API key: your credential for n8n / Make (optional)". The key is stored encrypted and not shown again afterwards. To change it, you create the webhook again.
Why do I see the signing secret only once?
The secret is treated like a password: it is stored encrypted and never handed out again after creation. Copy it into your target system right after saving. If it gets lost, simply create the webhook again and you get a new secret.
What happens if my receiver rejects the webhook?
Rechnungskit tries to deliver every message up to six times with growing intervals: starting at 15 seconds, doubling up to a maximum of one hour. If your receiver answers with a 2xx status, the message counts as delivered.
Every attempt is logged on the server with status code and error text. For persistent delivery problems, support can help you with the log.
Which events and data does the webhook contain?
For one-time purchases there are checkout.paid and checkout.failed. The JSON body contains, among other things, the product, amount and currency, the buyer's email and name, the delivery country, the invoice number (once created) and your passed-through metadata.
For subscriptions there are seven events of their own (subscription.activated to subscription.withdrawn) with a versioned envelope that always carries the complete subscription state. A unique event ID is in the body and in the header X-Rechnungskit-Event-Id, so you can detect duplicate deliveries.
Sources
Where to find it in the app
Recent changes
Rechnungskit is not a tax advisory or law firm. This article explains general principles and does not replace advice from a tax advisor (Steuerberater, § 5 StBerG) or a lawyer (§ 3 RDG). Rechnungskit is built for businesses based in Germany and prepares documents, tax rates and bookings automatically. How your specific case is treated remains your decision, ideally together with your tax advisor or a lawyer.