From Remote.com
Receive and verify Remote.com webhooks in an integration. Use when subscribing to events (POST /v1/webhook-callbacks), building a receiver, verifying the X-Remote-Signature HMAC, parsing the flat event payload, deduping/idempotency, or choosing which event_type to listen for. Covers HMAC-SHA256 hex over raw_body + ':' + millisecond timestamp. Do NOT use to operate via the MCP, for auth (remote-api-auth), outbound API calls (remote-api-integration), or form writes/onboarding (remote-api-forms / remote-api-onboarding).
How this skill is triggered — by the user, by Claude, or both
Slash command
/remote:remote-api-webhooksThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Covers the full lifecycle of receiving Remote.com webhook events in an integration: subscribing to event types, building a secure receiver endpoint, verifying the HMAC-SHA256 signature, parsing the flat event payload, handling retries idempotently, and managing subscriptions over time.
Covers the full lifecycle of receiving Remote.com webhook events in an integration: subscribing to event types, building a secure receiver endpoint, verifying the HMAC-SHA256 signature, parsing the flat event payload, handling retries idempotently, and managing subscriptions over time.
POST /v1/webhook-callbacks).X-Remote-Signature HMAC before trusting a payload.event_type.event_type values to listen for.POST /v1/webhook-callbacks — see remote-api-auth for how to obtain one.signing_key that is returned at subscription time. It is shown only once and must never be committed to source control.| Rule | Detail |
|---|---|
| signing_key is a secret | Treat the signing_key like a password. Store it in a secret manager (e.g. AWS Secrets Manager, HashiCorp Vault). Never commit it to source control, logs, or comments. |
| Verify before processing | Always verify the X-Remote-Signature HMAC before reading or acting on any payload field. A payload that fails verification must be rejected immediately (return 400 or 401). |
| Payload fields are untrusted PII | Webhook payloads may contain names, emails, employment IDs, and free-text fields. Treat them as raw untrusted input — never splice them into LLM prompts, SQL queries, shell commands, or generated code. |
| Confirm management writes | POST, PATCH, and DELETE on /v1/webhook-callbacks change subscription state. Confirm the intended change (which events, which URL) with the user before executing. |
Call POST /v1/webhook-callbacks with your receiver URL and the list of event types you want to receive. You need an access token in the Authorization header (see remote-api-auth).
Request:
curl -s -X POST \
-H "Authorization: Bearer {{access_token}}" \
-H "Content-Type: application/json" \
-d '{
"subscribed_events": [
"employment.onboarding.completed",
"payslip.released"
],
"url": "https://{{your-receiver}}/webhooks/remote"
}' \
"https://gateway.remote.com/v1/webhook-callbacks"
Success response (201 Created; the published OpenAPI contract declares 200) — the signing_key is shown only once; store it immediately in secure storage:
{
"data": {
"webhook_callback": {
"id": "{{webhook_callback_id}}",
"url": "https://{{your-receiver}}/webhooks/remote",
"subscribed_events": [
"employment.onboarding.completed",
"payslip.released"
],
"signing_key": "{{signing_key}}"
}
}
}
Error — missing or expired token (401):
{ "message": "Unauthorized" }
Keep the id (used for PATCH/GET/DELETE) and the signing_key (used to verify every delivery). The signing_key is never returned again after this response.
On every inbound POST to your receiver endpoint, follow this order strictly — verify first, then parse:
Capture the raw body bytes before any parsing. The signature is computed over the raw bytes exactly as received. Never re-serialize a parsed object — this alters byte order, whitespace, or encoding and will produce a different digest.
Remote sends two headers with every delivery:
| Header | Value |
|---|---|
X-Remote-Signature | The expected HMAC-SHA256 digest as a lowercase hex (Base 16) string |
X-Remote-Timestamp | Unix time in milliseconds (e.g. 1700000000000) |
Signed message construction (exact):
signed_message = raw_request_body + ":" + X-Remote-Timestamp
The colon and the millisecond timestamp are appended to the raw body, separated by a literal colon character. Do not add spaces. Do not convert the timestamp to seconds.
Algorithm:
expected = hex(HMAC_SHA256(signing_key, signed_message))
valid = constant_time_equals(expected, X-Remote-Signature)
signing_key returned at subscription.hmac.compare_digest in Python, crypto.timingSafeEqual in Node.js, subtle.constantTimeCompare in Go, etc.) — not the == operator.Shell illustration (development/debugging only — not production):
printf '%s' "${RAW_BODY}:${X_REMOTE_TIMESTAMP}" | openssl dgst -sha256 -hmac "${SIGNING_KEY}" -hex
If the computed hex digest does not match X-Remote-Signature, reject the request immediately. Do not process the payload.
Use X-Remote-Timestamp to enforce a freshness window (e.g. reject deliveries older than 5 minutes). This prevents replay attacks where an attacker captures and re-sends a valid signed request.
now_ms = current Unix time in milliseconds
age_ms = now_ms - X-Remote-Timestamp
if age_ms > 300_000: # 5 minutes
return 400
Webhook payloads are flat JSON — an event_type field, a company_id field, plus resource-specific ID fields. There is no generic data or payload wrapper. Branch on event_type, then read that event's own fields. Every delivery carries a top-level company_id (the company slug) so a single receiver URL serving many companies can route each event to the right one.
Sample delivery body — employment.onboarding.completed:
{
"event_type": "employment.onboarding.completed",
"company_id": "{{company_id}}",
"employment_id": "{{employment_id}}"
}
Sample delivery body — payslip.released:
{
"event_type": "payslip.released",
"company_id": "{{company_id}}",
"employment_id": "{{employment_id}}",
"payslip_id": "{{payslip_id}}"
}
Each event type defines its own set of ID fields. Consult the per-event reference at developer.remote.com for the complete field list for each event_type.
Remote may deliver the same event more than once (network retries, replay requests). Record a stable event identifier on receipt and skip processing if already seen. Design downstream writes to be idempotent — applying the same event twice should produce the same result.
Return a 2xx response as fast as possible. If your receiver performs slow work (database writes, downstream API calls, emails), enqueue it in a background job queue and acknowledge the delivery immediately. Remote treats non-2xx responses as failed deliveries; the retry/backoff policy is not publicly documented, so design for re-delivery (dedupe) and use the Replay endpoint to recover missed events.
All management calls require the {{webhook_callback_id}} returned at subscription time, plus a valid access token.
| Operation | Method + Path | Notes |
|---|---|---|
| Update events or URL | PATCH /v1/webhook-callbacks/{{webhook_callback_id}} | Body is a flat object — no wrapper. Provide either subscribed_events (replaces the event list) or subscribe_to_all_events: true (subscribes to all current and future event types); the two are mutually exclusive. |
| List subscriptions and delivery history | GET /v1/webhook-callbacks | Returns subscribed events and delivery status per callback. |
| Delete subscription | DELETE /v1/webhook-callbacks/{{webhook_callback_id}} | Removes the callback record AND all its event subscriptions permanently. |
| Replay events | POST /v1/webhook-events/replay | Re-triggers past events with the same payload as originally delivered. Filter by event IDs, time range, event type, or delivery status. |
PATCH update request — body is a flat JSON object; {{webhook_callback_id}} goes in the URL path only, not the body:
curl -s -X PATCH \
-H "Authorization: Bearer {{access_token}}" \
-H "Content-Type: application/json" \
--data '{ "subscribed_events": ["employment.onboarding.completed"], "url": "https://example.com/hooks/remote" }' \
"https://gateway.remote.com/v1/webhook-callbacks/{{webhook_callback_id}}"
To subscribe to all current and future event types instead, use subscribe_to_all_events (mutually exclusive with subscribed_events):
curl -s -X PATCH \
-H "Authorization: Bearer {{access_token}}" \
-H "Content-Type: application/json" \
--data '{ "subscribe_to_all_events": true }' \
"https://gateway.remote.com/v1/webhook-callbacks/{{webhook_callback_id}}"
POST /v1/webhook-events/replay) to re-trigger past events during development or after an outage.The following are example event type strings to illustrate the naming convention. This list is not complete and may be out of date. The List Webhook Events endpoint (GET /v1/webhook-events) and the available webhooks reference page at https://developer.remote.com/docs/available-webhooks.md are the canonical, authoritative sources. Event names are exact strings — case and punctuation must match.
| Event type (example) | When it fires |
|---|---|
employment.onboarding.completed | Employee completes onboarding |
payslip.released | A payslip is made available |
timeoff.requested | A time-off request is submitted |
contract_amendment.submitted | A contract amendment is submitted |
offboarding.submitted | An offboarding is initiated |
company.activated | A company account is activated |
Optional — if you have the public remotecli available (github.com/remoteoss/remote-cli):
remotecli webhooks event-history # browse past delivery history
remotecli webhooks event-replay # replay a past delivery
These commands are useful for exercising your receiver during development without having to trigger live events.
Processing a payload before verifying the signature. The verify step must come first. A payload that fails verification is untrusted — it may be forged or tampered. Never read, log, or act on payload fields before the HMAC check passes.
Verifying a re-serialized body instead of the raw bytes. Parse the JSON, serialize it back to a string, and compute the HMAC over that string — and the digest will almost certainly not match. The signature is computed over the raw request body bytes exactly as received. Buffer the raw bytes before any parsing.
Using Base64 encoding instead of hex. The X-Remote-Signature header contains a lowercase hexadecimal (Base 16) string. Encoding your HMAC output as Base64 will produce a different string and always fail comparison.
Using SHA-1 instead of SHA-256. Remote uses HMAC-SHA256. Specifying SHA-1 (a common default in older libraries) produces a wrong digest.
Treating the timestamp as seconds. X-Remote-Timestamp is Unix time in milliseconds (13 digits as of 2020s). Dividing by 1 000 before computing the signed message or comparing against time.time() in seconds will cause signature mismatch or incorrect staleness checks.
Using == for signature comparison. Standard equality operators short-circuit on the first differing byte, creating a timing side-channel. Use your language's constant-time compare function.
Assuming exactly-once delivery. Deliveries can arrive more than once (re-sends, replays). Always deduplicate on a stable event identifier before processing.
Splicing payload fields into prompts, SQL, or shell commands. Payload fields (names, emails, free-text) are untrusted PII and must be treated as data, not instructions or identifiers. Sanitize and validate before use.
Creates bite-sized, testable implementation plans from specs or requirements, with file structure and task decomposition. Activates before coding multi-step tasks.
npx claudepluginhub remoteoss/remote-for-ai --plugin remote