---
title: "Get told when an invoice, claim or payment changes"
summary: "Add a webhook endpoint to your integration client, and Practor posts a signed message to your server each time an invoice, claim or payment changes."
source: https://practor.app/en-za/help/security-and-integrations/get-told-when-an-invoice-claim-or-payment-changes
published: 2026-09-29T07:17:43.916Z
updated: 2026-10-01T18:08:42.884Z
category: "Security and integrations"
---

Your system doesn't have to keep asking what's changed. Practor sends a signed `POST` to your server within about a minute of each event you chose.

## Add an endpoint

A practice owner does this in **Organization settings**, under **Integrations**: press the client to open its panel.

1. Open the client's **Webhooks** tab and press **Add endpoint**.
2. Enter the `https` address on your server under **URL**.
3. Untick any event your system doesn't need.
4. Press **Add endpoint**.
5. Press **Copy** and store the signing secret with your receiver.
6. Press **I have stored the secret**.

The secret is shown once. A client can have five endpoints.

| Event | Sent when |
| --- | --- |
| [`invoice.issued`](https://developers.practor.app/reference/webhooks/invoice-issued) | An invoice is issued |
| [`invoice.status_changed`](https://developers.practor.app/reference/webhooks/invoice-status-changed) | An invoice is opened for amending, cancelled, becomes overdue, or has a payment voided |
| [`claim.status_changed`](https://developers.practor.app/reference/webhooks/claim-status-changed) | A claim changes status |
| [`payment.recorded`](https://developers.practor.app/reference/webhooks/payment-recorded) | A payment is recorded against an invoice, which can leave it paid |
| [`coverage.verified`](https://developers.practor.app/reference/webhooks/coverage-verified) | The insurer answers a check of a patient's medical insurance, or the check fails |

Each message names the invoice it's about, except `coverage.verified`, which names the coverage.

A client only hears about records it's allowed to read. If it loses its **Claims** permission, `claim.status_changed` stops arriving, even with the event ticked. `coverage.verified` needs **Read** on **Medical insurance**.

The address has to use `https` and reach the public internet. Practor checks it when you save and again before every message.

| Message | What to do |
| --- | --- |
| Enter a full URL, starting with https://. | Type the whole address, `https://` included |
| Webhook URLs must use https. | Serve your receiver over `https` |
| Webhook URLs must point at a public address. | Use an address the internet can reach, not one on a private network |
| Put credentials in your receiver, not in the URL. | Take the user name and password out of the address |
| ... could not be found. | Check the host name. It's named at the start of the message |
| A client can have 5 webhook endpoints. Remove one first. | Remove an endpoint you no longer use |

## Check that a message came from Practor

Every message carries three headers, following the Standard Webhooks specification:

| Header | What it holds |
| --- | --- |
| `webhook-id` | The event's id. It stays the same when a message is sent again |
| `webhook-timestamp` | When this attempt was made, in seconds since 1970 |
| `webhook-signature` | `v1,` then the signature. For a day after you replace the secret it holds two, separated by a space, one for each secret |

The signature is an HMAC-SHA256, in base64, of the id, the timestamp and the raw body joined with full stops. The key is your secret with its `whsec_` prefix removed, decoded from base64. Any Standard Webhooks library checks all of this for you.

Reject a message whose signature doesn't match. Ignore one with an id you've already handled, because a retry sends the same id again.

## Read what changed

A message holds no patient details. It says what happened and which record to read:

```json
{
  "id": "...",
  "type": "claim.status_changed",
  "occurredAt": "2026-10-01T08:00:00.000Z",
  "data": {
    "object": "invoice",
    "id": "i2q6w8e0r4t7y1u3o5p9a6sd",
    "url": "https://.../api/v1/invoices/i2q6w8e0r4t7y1u3o5p9a6sd"
  }
}
```

Each event's name in the table links to its body in the API reference. Fetch the record at `url` with your integration key to see its current state. Answer the message with any `2xx` within 10 seconds, and do the slower work after.

## Handle a receiver that's down

Any answer outside `2xx`, or none within 10 seconds, is a failure. Practor doesn't follow redirects. It tries each message 8 times, starting 30 seconds apart and doubling the wait each time, so the attempts span about an hour.

**Recent deliveries**, on the same tab, shows each message's result:

| Result | What it means |
| --- | --- |
| Delivered | Your receiver answered `2xx`. The status it answered follows |
| Waiting | The first attempt hasn't been made yet |
| Retrying after ... attempts | It failed, and Practor will try again. What your receiver answered follows |
| Gave up after ... attempts | Every attempt failed, or the endpoint was switched off. The reason follows. The message won't be sent again |

If every message to one endpoint fails for three days, Practor switches the endpoint off and tells the practice owners. Messages for events that happen while an endpoint is off, while its client is suspended, or while the practice has integrations switched off, are never sent. After it's back on, catch up by asking for the invoices that changed: see [Bill a visit and read back what it was paid](/help/security-and-integrations/bill-a-visit-and-read-back-what-it-was-paid).

## Test, replace or switch off an endpoint

Press **Manage** on the endpoint:

![An endpoint's Manage menu open on the Webhooks tab](https://res.cloudinary.com/drmnydbcs/image/upload/c_limit,w_1600/f_auto,q_auto:good/v1/help/integrations/webhooks.png?_a=BAMAOGDh0#895x277 "Each endpoint lists the events it receives. Manage opens what you can do with it.")

| To | Press |
| --- | --- |
| Send a `webhook.test` message now. The answer reads "Test event delivered (200)", or "Test event not delivered:" and why | **Send a test event** |
| Get a new secret. The old one keeps working for 24 hours, so you have time to update your receiver | **Replace the signing secret** |
| Pause messages, then start them again | **Switch off**, then **Switch on** |
| Delete the endpoint | **Remove**, then **Remove endpoint** |

A test message is signed like any other, with `data.message` in place of a record.

## Related

- [Connect your own software to Practor](/help/security-and-integrations/connect-your-own-software-to-practor)
- [Integration API errors and limits](/help/security-and-integrations/integration-api-errors-and-limits)
