Webhooks
Register an HTTPS URL and OfficeHero POSTs a signed JSON event to it whenever something you subscribed to happens.
Two webhook systems — make sure you are on the right page
| Integration webhooks (this section) | Notification webhooks | |
|---|---|---|
| For | Systems that integrate with OfficeHero data | Chat tools and simple alert receivers |
| Managed | With the API: /v1/webhook_endpoints and an API key with int:webhooks:manage | In the apps: Settings → Webhooks |
| Events | The event catalogue | The app’s notification events |
| Signature header | X-OH-Signature | X-Officehero-Signature |
Notification webhooks are described in Notification webhooks. Everything else in this section is about integration webhooks.
1. Create an endpoint
curl https://api.officehero.io/v1/webhook_endpoints \
-H "Authorization: Bearer $OFFICEHERO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "ERP sync",
"url": "https://erp.example.com/officehero-webhooks",
"event_types": ["fin.invoice.paid", "fin.invoice.voided"]
}'
The response is the endpoint plus its signing secret (whsec_…). It is
shown only in this response — store it with your other secrets. If you
lose it, rotate it.
urlmust behttps://and resolve to a public address. Private, loopback, link-local and reserved addresses, OfficeHero’s own addresses, hosts written as a number (127.1,2130706433,0x7f000001) and names that do not resolve are refused with422(url_not_publicly_routable). Deliveries are sent over IPv4: the host needs anArecord (a host with onlyAAAArecords is accepted but every delivery to it fails).event_typeslists exact types from the event catalogue. A type that is not in the catalogue is refused (422 unknown_event_type, listed inunknown_event_types) — so is an empty list.pinned_versionis optional; see Versioning.- Any other member is refused (
422,unknown_field).
An endpoint receives only events that happen after it was created.
Which events a key may subscribe to
A webhook never shows a key more than its GETs could. Besides
int:webhooks:manage, the key needs the read scope of every event family
it subscribes to — int:fin.invoices:read for fin.invoice.*,
int:fm.vehicles:read for fm.vehicle.*, and so on; the
event catalogue shows the scope of each event. A
missing one is a 403 with missing_scope (and missing_scopes when several
are missing).
Events with no matching read endpoint — compliance, e-invoicing, DATEV
exports, tachograph, dashcam and e-signature — and ["*"] (every event) need
a key with int:*. Messaging and identity events need
int:messaging.messages:read and int:identity.users:read. Settings → API
keys offers none of these three; ask support if you need one.
The key’s scopes and state are checked again for every event: when a key is revoked or expires, its endpoints stop receiving events at once (see Who owns an endpoint).
2. Receive and verify
Each delivery is a POST with a JSON envelope. Your
endpoint must:
- Verify the signature over the raw body before trusting anything in it — see Verifying signatures, with ready-made code for seven languages.
- Answer
2xxwithin 15 seconds. Do the work afterwards (queue it). - Deduplicate on the envelope
id. A delivery can arrive more than once.
Anything other than 2xx is retried, except 410 Gone — see
Retries.
Who owns an endpoint
An endpoint belongs to the API key that created it. Only that key can list,
read, change, pause, resume, delete or replay it; to any other key it does not
exist (404). A key with int:* manages every endpoint of the company.
Revoking a key pauses its endpoints (status: paused,
disabled_reason: api_key_revoked). A key that expires (or is deleted)
leaves them active until the first event it can no longer receive, which
pauses them with disabled_reason: api_key_inactive. Either way they cannot
be resumed while the key is revoked or expired (409 owner_key_inactive). When you replace a key that
owns endpoints, create the endpoints again with the new key before you
revoke the old one, then delete the old endpoints. See
Authentication.
Endpoint statuses
| Status | Meaning |
|---|---|
active | Receives deliveries. |
paused | You paused it (POST …/pause), or its API key was revoked (disabled_reason: api_key_revoked) or expired (api_key_inactive). Events that happen while it is paused are not queued for it. |
disabled | Deleted by you (disabled_reason: user_deleted), or switched off automatically after failures (auto_consecutive_failures, auto_410_repeat, auto_7_day_all_dead — see Retries). |
POST …/resume sets a paused endpoint, or one switched off after failures,
back to active, with two exceptions: an endpoint whose key is revoked or
expired (409 owner_key_inactive), and an endpoint switched off after
failures when the company’s quota is full — resuming it takes a place in the
quota again (429 quota_exceeded).
A deleted endpoint does not come back. Pausing, resuming or deleting it
again answers 404; it is still listed, as disabled with
disabled_reason: user_deleted, so that its delivery history stays readable.
To receive events at that URL again, create a new endpoint (it gets a new
secret).
Pausing an endpoint that is already paused keeps its disabled_reason, so an
endpoint paused because its key was revoked still says api_key_revoked.
Pausing does not hold deliveries that are already queued. A delivery
waiting for its first attempt or a retry ends failed with
error_class: endpoint_disabled when its turn comes. After you resume, send
those again with replay_failed.
Limits
A company can have 5 endpoints that are active or paused. Creating
another one — or resuming or pausing one switched off after failures — answers
429 with
detail_key: errors.integrations.webhooks.quota_exceeded and no Retry-After
until you delete one. Ask support if you need more.
Events with personal data
Event types marked PII in the event catalogue
carry personal data — employee identifiers, driver and location data,
whistleblowing and pay-transparency records. They are delivered only to an
endpoint that is enabled for personal data; to an endpoint without that
permission they are skipped. To enable it, contact support with
the endpoint id and why it needs the data. Changing an endpoint’s url or
event_types withdraws the permission.
Managing endpoints
| Action | Request |
|---|---|
| List | GET /v1/webhook_endpoints |
| Read | GET /v1/webhook_endpoints/{id} |
| Change name, URL, events, version | PATCH /v1/webhook_endpoints/{id} |
| Pause / resume | POST /v1/webhook_endpoints/{id}/pause, …/resume |
| Rotate the secret | POST /v1/webhook_endpoints/{id}/rotate_secret |
| Delete | DELETE /v1/webhook_endpoints/{id} |
| See deliveries | GET /v1/webhook_endpoints/{id}/deliveries |
| Send again | Replaying deliveries |
All of them need an API key with int:webhooks:manage. Details and “Try it”
are in the reference. The “Try it” example
for creating an endpoint uses receiver.example.invalid, which is refused on
purpose so that trying it cannot register a live receiver — put your own URL
there.