Webhooks
Webhooks let your system receive real-time notifications when events happen in DidYouGo, instead of polling. When an event occurs, DidYouGo sends an encrypted HTTP POST to the URL you register.
Note: There is currently no webhook event for bulk-import / background-task
completion. Background imports are still tracked by polling the
Background Processing endpoints. An
import.completed event is a natural future addition.
Register a Webhook
POST /api/webhooks/register
Register an endpoint to receive events. The API generates a secret server-side and uses it to
sign every
delivery to your endpoint. The secret is returned when the webhook is created, and can also be retrieved via
GET endpoint.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
organization_uuid |
string | Yes | UUID of the organization that owns the webhook |
url |
string | Yes | HTTPS endpoint that will receive the events |
events |
array[string] | Yes | Event names to subscribe to — see the catalog below |
is_active |
boolean | No | Enable/disable delivery (default: true) |
Manage Webhooks
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/webhooks |
List webhooks for your organization |
| GET | /api/webhooks/uuid/{uuid} |
Get a single webhook |
| PATCH | /api/webhooks/uuid/{uuid} |
Update URL, events, or active state |
| DELETE | /api/webhooks/uuid/{uuid} |
Delete a webhook |
Event Catalog
| Event | Description | Category |
|---|---|---|
service_confirmation.check_in |
Service confirmed via check-in | Service Confirmation |
service_confirmation.check_out |
Service confirmed via check-out | Service Confirmation |
service_confirmation.delivery |
Service delivered | Service Confirmation |
Delivery Format
Every delivery is an HTTP POST with Content-Type: application/json. The payload in
the
body is encrypted (AES-256-CBC) and signed with HMAC-SHA256.
| Item | Value |
|---|---|
| Request body | {"payload": "<encrypted-base64>"} |
| Signature header | X-DidYouGo-Signature: sha256=<hmac-hex> |
The plaintext event envelope before encryption is:
{
"id": "evt_6f4b2a8e-1c3d-4e5f-9a0b-cdef12345678",
"type": "service_confirmation.check_in",
"data": {
"uuid": "30f8c2a5-5f6e-4c9a-b3d1-7e2a9c4f8d12",
"status": "CHECK_IN",
"location": "123 Main Street",
"checked_in_at": "2026-08-18T04:30:00.000Z"
},
"created_at": "2026-08-18T04:30:00.000Z"
}
The data object is the full service confirmation resource — the same shape is delivered for all
three service confirmation events. See the complete example in Event Payload.
Event Payload
The data object is the full service confirmation resource, marshalled the same way for all three
events. The only differences are the status value and which timestamps / image fields are
populated. Complete example for service_confirmation.check_in:
{
"id": "evt_6f4b2a8e-1c3d-4e5f-9a0b-cdef12345678",
"type": "service_confirmation.check_in",
"created_at": "2026-08-18T04:30:00.000Z",
"data": {
"uuid": "30f8c2a5-5f6e-4c9a-b3d1-7e2a9c4f8d12",
"created_on": "2026-08-18 04:30:00.000000",
"updated_on": "2026-08-18 04:30:00.000000",
"organization_uuid": "Po5fQfUGec5ucKFeMDes24",
"user_uuid": "U5a7KbVpXw3mRtQnYcDfGh1",
"company_uuid": "Cm3nTz9LxVb2QwErTyUiOp0",
"qr_code_uuid": "Qr7xZc1vBnMqWeRtYuIoPa2",
"qr_code_tag_uuid": "Tg9kLp3mNvBcXzQwErTyUiOa",
"service_uuid": "Sv2fGh8jKlQwErTyUiOpAsDf",
"service": "Home Care",
"appointment_schedule_uuid": "Ap5dSf7gHjKlQwErTyUiOpZx",
"status": "CHECK_IN",
"location": "123 Main Street, Northcote VIC 3070",
"location_verified": true,
"checked_in_at": "2026-08-18T04:30:00.000Z",
"checked_out_at": null,
"note": "Client home care visit completed",
"work_mode": "IN_PERSON",
"is_sms": false,
"delivery_image_url": null,
"checkout_image_urls": null,
"manual_distance_from_previous": 0,
"calculated_distance_from_previous": 0,
"previous_service_confirmation_uuid": null,
"user_geolocation": {
"address": "123 Main Street, Northcote VIC 3070",
"lat": -37.7671,
"lng": 145.0014
},
"user": {
"uuid": "U5a7KbVpXw3mRtQnYcDfGh1",
"firstname": "John",
"lastname": "Doe",
"email": "john.doe@example.com"
},
"company": {
"uuid": "Cm3nTz9LxVb2QwErTyUiOp0",
"name": "Northcote Community Care"
},
"appointment_schedule": {
"job_reference_id": "JOB-2026-0042",
"start_date_time": "2026-08-18T04:00:00.000Z",
"end_date_time": "2026-08-18T05:00:00.000Z"
},
"qr_code_tag": {
"tag_display_name": "Mr. John Doe",
"tag_name": "john-doe-home",
"tag_number": "TAG-00123"
},
"services": [
{
"uuid": "Sv2fGh8jKlQwErTyUiOpAsDf",
"name": "Home Care",
"code": "HC",
"service_categories": [
{
"uuid": "Sc8mNvBcXzQwErTyUiOp12",
"name": "Personal Care"
}
]
}
],
"question_responses": [
{
"response_text": "Satisfied",
"question": {
"question_text": "How would you rate today's service?",
"question_type": "RATING"
}
}
],
"previous_service_confirmation": null
}
}
The other two events deliver the identical shape; the fields that differ are:
| Event | status |
Populated fields |
|---|---|---|
service_confirmation.check_in |
CHECK_IN |
checked_in_at, location_verified, user_geolocation |
service_confirmation.check_out |
CHECK_OUT |
checked_in_at + checked_out_at, checkout_image_urls,
manual_distance_from_previous, calculated_distance_from_previous
|
service_confirmation.delivery |
DELIVERY |
delivery_image_url, is_sms |
Verify a Delivery
Every delivery is encrypted with AES-256-CBC using the first 32 hex characters of the signature value
(the part after sha256=) as the encryption key — so the payload can always be decrypted from
the
signature header alone. Additionally, the plaintext is signed with HMAC-SHA256 using the webhook
secret that DidYouGo holds server-side. HMAC re-verification requires that secret, so full
signature verification is performed by DidYouGo's own receiver; a client without the secret can decrypt the
payload but cannot independently re-validate the HMAC.
Python (decrypt + verify HMAC)
import base64
import hashlib
import hmac
import json
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import padding
secret = "your-webhook-secret" # held by DidYouGo; not returned by the API
body = {"payload": "<encrypted-base64>"} # from the POST body
sig_header = "sha256=<hmac-hex>" # from X-DidYouGo-Signature
signature = sig_header.split("=")[1] # hmac hex
key = signature[:32].encode() # AES-256 key from signature
# 1. Decrypt payload (first 16 bytes are the IV)
data = base64.b64decode(body["payload"])
iv, ciphertext = data[:16], data[16:]
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
decryptor = cipher.decryptor()
plain = decryptor.update(ciphertext) + decryptor.finalize()
unpadder = padding.PKCS7(128).unpadder()
plain_json = unpadder.update(plain) + unpadder.finalize()
event = json.loads(plain_json)
# 2. Verify HMAC over the decrypted JSON (requires the webhook secret)
expected = hmac.new(secret.encode(), plain_json, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, signature):
raise Exception("Invalid signature — reject the delivery")
Node.js (HMAC verification)
If you need signature verification and hold the webhook secret, HMAC-SHA256 over the decrypted
JSON works as follows — note the full verification still requires AES decryption as above:
const crypto = require("crypto");
const secret = "your-webhook-secret";
const signature = sigHeader.split("=")[1]; // value after "sha256="
const decryptedJson = decryptAesCbc(signature.slice(0, 32), body.payload); // see Python example
const expected = crypto
.createHmac("sha256", secret)
.update(decryptedJson)
.digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
throw new Error("Invalid signature — reject the delivery");
}
Security
- Always respond to a delivery before processing it fully, and respond to
2xxas quickly as possible to acknowledge delivery. - Decrypt payloads with the signature-derived key (first 32 hex chars of the
X-DidYouGo-Signaturevalue). - Reject any delivery where decryption fails or the signature is malformed.
Example Request
curl -X POST http://staging.didyougo.com.au/api/webhooks/register \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your_api_key:your_api_secret" \
-d '{
"organization_uuid": "org-uuid-here",
"url": "https://your-server.com/webhooks/didyougo",
"events": ["qr_code.created", "service_confirmation.check_in"],
"is_active": true
}'
Try It
Requests are sent without cookies — you must provide a valid Bearer <api_key>:<api_secret> header.
Click the Send API Request button above and see the response here!
Example Response
{
"uuid": "f7JEh4kzKqFGkrFnzPfLs3",
"created_on": "2026-08-18 04:33:52.039245",
"updated_on": "2026-08-18 04:33:52.039245",
"organization_uuid": "Po5fQfUGec5ucKFeMDes24",
"url": "http://localhost:8080/hook",
"events": [
"service_confirmation.check_in",
"service_confirmation.check_out",
"service_confirmation.delivery"
],
"is_active": true,
"secret": "SS9CAcZapDXLVmPR3fEJ"
}