Docs menu

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
ForSystems that integrate with OfficeHero dataChat tools and simple alert receivers
ManagedWith the API: /v1/webhook_endpoints and an API key with int:webhooks:manageIn the apps: Settings → Webhooks
EventsThe event catalogueThe app’s notification events
Signature headerX-OH-SignatureX-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.

  • url must be https:// 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 with 422 (url_not_publicly_routable). Deliveries are sent over IPv4: the host needs an A record (a host with only AAAA records is accepted but every delivery to it fails).
  • event_types lists exact types from the event catalogue. A type that is not in the catalogue is refused (422 unknown_event_type, listed in unknown_event_types) — so is an empty list.
  • pinned_version is 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:

  1. Verify the signature over the raw body before trusting anything in it — see Verifying signatures, with ready-made code for seven languages.
  2. Answer 2xx within 15 seconds. Do the work afterwards (queue it).
  3. 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

StatusMeaning
activeReceives deliveries.
pausedYou 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.
disabledDeleted 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

ActionRequest
ListGET /v1/webhook_endpoints
ReadGET /v1/webhook_endpoints/{id}
Change name, URL, events, versionPATCH /v1/webhook_endpoints/{id}
Pause / resumePOST /v1/webhook_endpoints/{id}/pause, …/resume
Rotate the secretPOST /v1/webhook_endpoints/{id}/rotate_secret
DeleteDELETE /v1/webhook_endpoints/{id}
See deliveriesGET /v1/webhook_endpoints/{id}/deliveries
Send againReplaying 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.