Skip to main content
Instead of polling, give GuidingHand an HTTPS endpoint. It sends a signed POST when something changes in your org’s sessions and tasks. Each org has one endpoint.

Set the endpoint

Set it in the console (Settings → Webhooks), or with PUT /v1/webhook. Leave events empty to get every type, or list the ones you want; an unknown type is a 400. Only the fields you send change: leave out url to keep the endpoint, leave out events to keep the filter, and send { "url": null } to remove the endpoint. It needs the admin role or an API key.
Response
secret is returned only when it is first made, or when you send "rotate_secret": true. Store it right away. GET /v1/webhook shows has_secret but never the secret itself. Changing the URL or events later keeps the same secret.
  • To rotate the secret, send { "rotate_secret": true } (the URL and events stay as they are). The old secret stops working at once, so deploy the new one to your receiver first or accept both for a moment.
  • To remove the endpoint, send {"url": null}.
  • GET /v1/webhook returns the current url, events, has_secret and the list of event_types.

Public HTTPS only

The URL must use https and resolve to a public address. Private and local addresses (10.x, 127.x, 172.16–31.x, 192.168.x, localhost, *.local, *.internal and the like) are refused with 400 invalid_request, and the check runs again on every delivery. Redirects are not followed. For local development, expose your receiver through a tunnel with a public HTTPS URL.

Event types

A task that asks several questions sends task.waiting_for_user and task.question_answered for each, and likewise task.waiting_for_approval and task.approval_decided for each approval. task.question_answered tells you what the answer was and who gave it: answered_by is customer (the person at the computer, in the GuidingHand app) or operator (your team, through the API or the console). Use it to close the question in your own tool when the customer answered it first. data.task is the task just after the answer, so its pending is null.
task.approval_decided does the same for approvals. data.decision has the approval_id, the decision (approve or deny), your team’s note (or null) and answered_by:

Payload

Every event has the same envelope. data.task and data.session are the same objects the API returns.
Use metadata to find your ticket, and id to ignore an event you’ve already handled.

Verify the signature

Every request has a GuidingHand-Signature header:
t is the Unix time the request was signed. v1 is the hex HMAC-SHA256 of "{t}.{raw body}", keyed with your endpoint’s whole secret (including the whsec_ prefix). To verify:
  1. Read the raw request body, before any JSON parsing. Re-serialized JSON won’t match.
  2. Compute the HMAC of t, a ., and the raw body with your secret.
  3. Compare it with v1 in constant time.
  4. Reject the request if t is more than 5 minutes from your clock. This stops replays of old requests.
Answer 2xx quickly and do slow work afterwards, so the delivery doesn’t time out.

Delivery and retries

  • Each delivery waits up to 10 seconds for your answer. Any 2xx counts as received.
  • On a timeout, a network error or any other status (including 3xx), GuidingHand retries up to 4 times, after about 5 seconds, 20 seconds, 1 minute and 2 minutes: about 3 minutes in total. Then it gives up on that event.
  • A retry sends the same body with the same id, and a fresh t and signature.
  • Events can arrive out of order, and more than once. Use id to skip duplicates, and trust the status and updated_at inside data rather than the order of arrival. When in doubt, GET the task or session.
Webhooks are a signal, not the record. If your endpoint was down for longer than the retries, the API still has everything: list recent tasks with GET /v1/tasks to catch up.