Webhook delivery
Webhooks push events to your endpoint. Each delivery is signed so you can prove it came from OpesCare, and failed deliveries are retried before they are marked failed.
The flow
Scroll through the steps below the diagram, or hover a step, to highlight it in the diagram.
- Event recorded, delivery queued Each active subscription for the event type gets a queued delivery.
- Sign: HMAC-SHA256(t.payload) The timestamp and JSON body are signed with your subscription secret.
- Public address check Callbacks that resolve to private or local addresses are refused; redirects are not followed.
- POST JSON + signature headers Headers: X-OpesCare-Signature (t=..., v1=...), X-OpesCare-Timestamp, X-OpesCare-Event-Id.
- Verify signature, dedupe Recompute the HMAC, reject old timestamps, and ignore an Event-Id you already processed.
- 2xx: delivered Any 2xx within 10 seconds marks the delivery as delivered.
- Retry after 1 min, then 5 min A non-2xx answer or a timeout is retried, up to three attempts in all.
- Marked failed, replayable After the last attempt the delivery is failed; replay it from the developer portal.
- Address refused A callback URL that resolves to a private, loopback or link-local address is never called.
How this diagram is verified 21 of 21 checks pass
The picture, the steps, the PDF and the JSON are generated from one specification. An automated test proves the graph and checks every reference below against the running application.
- Every card has a unique id
- Every card has a known kind
- Every arrow joins two existing cards
- Exactly one starting point (a person, device or partner)
- Every card is reachable from the start
- Every card leads to an outcome (no dead ends)
- No cycles except declared retry loops
- Every numbered step follows real arrows
- Outcomes end the flow
- No two cards share a grid cell
- Every card names its evidence
- Every route, class, method and setting exists
- Every icon is from the Lucide set
- Every icon fits its card kind
- Every label exists in English and French
- Every label fits its card
- No arrow crosses a card
- No two unrelated arrows run on top of each other
- No arrow label covers a card or another label
- Every arrowhead is visible
- Connected cards of different kinds never share a colour
| Card | Evidence in the code |
|---|---|
| Event recorded, delivery queued | method:App\Services\WebhookService::dispatch class:App\Models\WebhookSubscription |
| Sign: HMAC- SHA256(t.payload) | method:App\Jobs\SendWebhookJob::handle class:App\Models\WebhookSecret |
| Public address check | method:App\Support\PublicUrlGuard::isAllowed method:App\Support\PublicUrlGuard::isPublicIp |
| POST JSON + signature headers | class:App\Models\WebhookDeliveryLog route:docs.webhooks |
| Address refused | method:App\Support\PublicUrlGuard::resolve |
| 2xx: delivered | class:App\Models\WebhookDeliveryLog |
| Verify signature, dedupe | route:docs.webhooks route:portals.developer.webhooks |
| Marked failed, replayable | method:App\Jobs\SendWebhookJob::failed route:portals.developer.webhooks.replay method:App\Services\WebhookService::replay |
| Retry after 1 min, then 5 min | property:App\Jobs\SendWebhookJob::tries property:App\Jobs\SendWebhookJob::backoff |
What this guarantees
- You can verify every delivery came from OpesCare and was not altered.
- Webhooks are never sent to private or internal addresses.
- Failed deliveries are retried and can be replayed; dedupe on the event ID.
Where it lives in the code
method:App\Services\WebhookService::dispatchclass:App\Models\WebhookSubscriptionmethod:App\Jobs\SendWebhookJob::handleclass:App\Models\WebhookSecretmethod:App\Support\PublicUrlGuard::isAllowedmethod:App\Support\PublicUrlGuard::isPublicIpclass:App\Models\WebhookDeliveryLogroute:docs.webhooksmethod:App\Support\PublicUrlGuard::resolveroute:portals.developer.webhooksmethod:App\Jobs\SendWebhookJob::failedroute:portals.developer.webhooks.replaymethod:App\Services\WebhookService::replayproperty:App\Jobs\SendWebhookJob::triesproperty:App\Jobs\SendWebhookJob::backoff