Most webhook advice is about reliability: retries, signatures, idempotency. All of it applies. But when the thing that changed is someone's home address or email, there is a second set of questions, about what happens when the webhook, the log it lands in or the account behind it falls into the wrong hands.
I run ContaShare, where a person keeps their contact details in one account and chooses which companies can read them. When they change a detail, each company they share it with gets a webhook. Here are six decisions from building that, and what each one cost.
1. The webhook carries no personal data
This is the whole payload:
{
"id": "e5aeb9bf-5676-41bc-9261-fc88920aaa56",
"type": "address",
"action": "update",
"resource_url": "https://contashare.com/api/v1/address/e5aeb9bf-5676-41bc-9261-fc88920aaa56"
}
No address, no name, no customer ID. The receiver fetches resource_url with that customer's OAuth token to get the new version.
Putting the new address in the payload would save the receiver a request. But webhook bodies end up in places nobody audits: request logs, error trackers, a tunnelling tool's history, the retry queue of whatever sits in front of the app. A payload with nothing in it can be logged anywhere.
It also means permission is checked at the moment of reading, not the moment of sending. If the customer stops sharing between the webhook and the fetch, the fetch returns 404 and the company never sees the new address.
The cost: the receiver has to store which customer each record id belongs to, and make a second request.
2. Every webhook means "fetch the latest"
A payload with values in it has an ordering problem. Two changes a minute apart, delivered out of order after a retry, leave the receiver holding the older one.
A payload that only says "this record changed" doesn't. Two webhooks for the same record are identical, handling one twice is harmless, and whichever arrives last still fetches the current version. Receivers can fold queued duplicates into one fetch, and I tell them to.
The cost: you can't offer a history of changes through the webhook. For contact details nobody has asked for one.
3. Each company gets its own ID for the same person
GET /api/v1/me returns the ID a company knows a customer by. It never changes for that company, and every other company gets a different one for the same person.
A single global user ID is easier to build and to debug. It is also a join key: any two companies holding it could match their customer lists against each other without asking anyone. Giving each company its own ID removes that option entirely.
The cost: support is harder. When a company quotes an ID at me, it only means something alongside which company is asking.
4. A change can wait an hour, on purpose
This is the one that surprised me most while building it.
A service that updates someone's email at every company at once is a very good target. Many sites reset a password by emailing the address on file. So if someone gets into a ContaShare account and changes the email, every connected company would be told the attacker's address, and the attacker then asks each one for a password reset.
So a change to anything a company already holds waits an hour before it goes anywhere. The owner is emailed at once, at the old address too, with a link that cancels the change and locks the thief out. Until the hour is up there is no webhook and the API keeps returning the version the company already has.
A change confirmed with an established passkey or two-factor, or with a link emailed to the owner, skips the wait. "Established" matters: a second factor added in the last 72 hours doesn't count, or the thief would add their own and carry on.
The cost: a real customer with no second factor waits an hour for their new address to arrive. I kept it short for that reason. It only needs to be long enough for the owner to see the warning.
5. The receiver is told not to trust the change either
The hold reduces the risk and doesn't remove it. So the docs say it plainly: treat an update as new contact details, not as proof of who someone is. Before a changed email or phone number is used to reset a password or send a login code, tell the old address and ask the customer to confirm in your own app.
There is a webhook for the worst case too. If an account is reported stolen, it is locked, and each company gets locked for the records it holds. Fetches return 423 until unlocked arrives. The advice on locked is to treat anything received for that customer in the last 7 days as possibly not from them.
The cost: the integration guide is longer and less cheerful than a marketing page would like. I think that's the right way round.
6. Stopping sharing sends nothing
When a customer stops sharing a record with a company, that company gets no webhook. Its next fetch of the record returns 404, the same as for a record that was deleted, and its token keeps working for anything else the customer still shares.
A "revoked" webhook would be tidier for the receiver. But it would announce a decision the customer may not want announced, and it can't be something a company's endpoint is able to delay or fail. Stopping sharing takes effect straight away, with nothing on the other side to wait for.
The cost: receivers must handle 404 as "remove this", which the guide repeats in three places.
The ordinary parts still matter
For completeness, the reliability basics are all there:
- Every request has a
Signatureheader: the HMAC-SHA256 of the raw body with the team's secret. Check it against the raw body, with a constant-time comparison. - A failed delivery is tried 3 times, 10 and then 100 seconds apart. After that it sits in a delivery history where the company can send it again once their endpoint is fixed.
- Webhooks are free and so are failed API calls. A company pays for a successful read: 1.25p for an address.
What I'd tell someone building the same thing
Ask what your webhook is worth to someone who shouldn't have it, and what your API does for someone who took over an account. For most data the answer is "not much", and a full payload is fine. For anything another site would use to decide who someone is, the answers shape the design more than the retry policy does.
If you want to see how it reads from the receiving side, the webhooks guide and API reference are public, and there are packages for Laravel, Node and Python. New company accounts get 1,000 free credits with no card: contashare.com/businesses.
I'd welcome holes being picked in any of this, especially number 4.
Top comments (0)