Webhooks
A successful checkout is not the end of a case: the quote is reviewed afterwards and can still be canceled or rejected. Instead of polling, register an HTTPS endpoint and PartnerHub sends you an event every time the status of one of your financing cases changes. This guide covers the registration, the events, the payload, the signature check and how deliveries are retried.
Build it step by step
POST
/api/webhook_endpoints
Register the URL that should receive events. It must be a publicly reachable
https URL. Sandbox and production are separate environments:
register your endpoint in each one you use.
curl -X POST 'https://sandbox.partner.miete24.com/api/webhook_endpoints' \
-H 'X-Api-Key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{ "url": "https://example.com/partnerhub/webhook" }'
const response = await fetch('https://sandbox.partner.miete24.com/api/webhook_endpoints', {
method: 'POST',
headers: {
'X-Api-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({ url: 'https://example.com/partnerhub/webhook' }),
});
const endpoint = await response.json();
// Store endpoint.secret now: it is never returned again.
{
"id": "01K70PZ3QF8W6N2D4R7T9V1X5B",
"url": "https://example.com/partnerhub/webhook",
"active": true,
"secret": "9b1d4c7e2a5f8b0c3d6e9f1a4b7c0d2e5f8a1b4c7d0e3f6a9b2c5d8e1f4a7b0c",
"createdAt": "2026-10-07T09:00:00+02:00"
}
The secret is shown only once,
in this response. Store it right away; you need it to verify every delivery. To change it,
delete the endpoint and register it again. Pause deliveries with
PATCH /api/webhook_endpoints/{id} and
"active": false; list and delete your endpoints with
GET and DELETE.
Errors:
422 when the URL is missing, not https
or not publicly reachable;
409 when this URL is already registered or you already have
20 endpoints;
401 for a missing or invalid API key.
All endpoint operations & code samples → Webhook Endpoint reference
Know the events
PartnerHub currently sends one event type. It fires on every status change of a financing
case: when its quote, contract or quote request moves to a new status. That includes quotes
you created with "submitFinancing": false.
| Event | Sent when |
|---|---|
| financing_case.status_changed | The status of one of your financing cases changes. |
To react to a canceled or rejected order, filter on data.toStatus:
| data.toStatus | What happened |
|---|---|
canceled |
The case was canceled: by you via POST /api/financing_cases/{id}/cancel, by the Miete24 approval team during its review, or automatically when requested documents stay overdue. |
finance_rejected |
The lender declined the financing request. |
rejected_by_customer |
The end customer rejected the quote. |
Every other status (for example finance_accepted or
completed) arrives the same way; the full list is on the
Lifecycle page.
One cancellation can produce two events: when a case already has a contract, both the
contract and the quote are canceled, and each sends its own event.
Read the payload
Each event is a JSON POST to your URL. This is a cancellation
during the review:
{
"id": "01K70Q8V6M3J5Z2R9T4X7B1C8D",
"event": "financing_case.status_changed",
"occurredAt": "2026-10-07T09:15:00+02:00",
"version": 1,
"data": {
"financingCaseId": "019e964c-fb93-73a6-a672-254c170c72d9",
"fromStatus": "finance_review",
"toStatus": "canceled",
"status": "canceled",
"transition": "cancel",
"quoteId": "019e964c-fb94-7c1e-9f0a-3b5d8e2a6c41"
}
}
| Field | Meaning |
|---|---|
| id | Unique event id. It stays the same when a delivery is retried, so use it to ignore duplicates. |
| event | The event type. |
| occurredAt | When the status changed (ISO 8601). |
| version | Version of this envelope, currently 1. |
| data.financingCaseId | The financeCaseId returned by the checkout. |
| data.quoteId | The quote of the case. Omitted when the case has no quote. |
| data.fromStatus | Status before this change. |
| data.toStatus | Status after this change. |
| data.status | Status of the financing case right after this change. A retry sends the same value. |
| data.transition | Name of the change, for example cancel or finance_reject. |
The payload does not include a reason for a cancellation. New fields may be added to
data without a new version; ignore fields you don't know (see
Versioning & Deprecation).
Verify the signature
Every request carries an X-Webhook-Signature header: the
base64-encoded HMAC-SHA256 of the raw request body, keyed with your endpoint's secret.
Compute it over the body exactly as you received it, before parsing the JSON, and reject
the request when it does not match.
import crypto from 'node:crypto';
/**
* Verifies the X-Webhook-Signature header of a PartnerHub webhook request.
*
* rawBody must be the request body as a Buffer, exactly as received,
* before any JSON parsing. secret is the signing secret returned once
* when you registered the endpoint.
*/
export function verifyPartnerHubSignature(rawBody, signatureHeader, secret) {
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest();
const received = Buffer.from(signatureHeader ?? '', 'base64');
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}
// Usage with Express:
// app.post('/partnerhub/webhook', express.raw({ type: 'application/json' }), (req, res) => {
// if (!verifyPartnerHubSignature(req.body, req.get('X-Webhook-Signature'), secret)) {
// return res.sendStatus(401);
// }
// const event = JSON.parse(req.body);
// res.sendStatus(204);
// });
<?php
/**
* Verifies the X-Webhook-Signature header of a PartnerHub webhook request.
*
* $rawBody must be the request body exactly as received, before any JSON
* decoding. $secret is the signing secret returned once when you registered
* the endpoint.
*/
function verifyPartnerHubSignature(string $rawBody, string $signatureHeader, string $secret): bool
{
$expected = base64_encode(hash_hmac('sha256', $rawBody, $secret, true));
return hash_equals($expected, $signatureHeader);
}
// Usage in plain PHP:
// $rawBody = file_get_contents('php://input');
// $signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
// if (!verifyPartnerHubSignature($rawBody, $signature, $secret)) {
// http_response_code(401);
// exit;
// }
Respond and handle retries
-
Answer with any
2xxstatus within 5 seconds. Do longer work after you have answered. - A different status, a timeout or a connection error counts as failed. PartnerHub retries up to 3 times: the first retry after 2 seconds, each further wait 2 times longer, at most 30 seconds. After the last retry the event is not sent again.
-
Redirects are not followed: a
3xxanswer counts as failed, so register the final URL. -
Events can arrive more than once and not always in order: a retried event can arrive after a
newer one. Ignore an
idyou have already processed. Treatdata.statusof the event with the latestoccurredAtas the current state and ignore older events that arrive late; when in doubt, read the case withGET /api/financing_cases/{id}. -
If you missed an event, read the case at any time with
GET /api/financing_cases/{id}(see the Financing Case reference).
Sandbox: trigger a cancellation
Register an endpoint with your sandbox API key, check out a test cart, then cancel the case
with the sandbox-only simulate endpoint. It applies the same cancellation as a real one, so
your endpoint receives a financing_case.status_changed event with
"toStatus": "canceled". Use
"scenario": "finance_rejected" to test a declined financing request.
POST
/api/sandbox/financing_cases/{id}/simulate
curl -X POST 'https://sandbox.partner.miete24.com/api/sandbox/financing_cases/019e964c-fb93-73a6-a672-254c170c72d9/simulate' \
-H 'X-Api-Key: YOUR_SANDBOX_API_KEY' \
-H 'Content-Type: application/json' \
-d '{ "scenario": "canceled" }'
const response = await fetch(
'https://sandbox.partner.miete24.com/api/sandbox/financing_cases/019e964c-fb93-73a6-a672-254c170c72d9/simulate',
{
method: 'POST',
headers: {
'X-Api-Key': 'YOUR_SANDBOX_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({ scenario: 'canceled' }),
},
);
// 204 No Content on success; the webhook follows.
Every scenario is listed in the Sandbox Simulate guide.
What's next
- See which checkout requests are rejected up front in the Financing & Solvency guide.
- See the complete state machine on the Lifecycle page.
- New to the API? Start with the Create a Quote guide.