Webhooks

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:

json
{
  "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:

json
{
  "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)

python
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:

javascript
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 2xx as quickly as possible to acknowledge delivery.
  • Decrypt payloads with the signature-derived key (first 32 hex chars of the X-DidYouGo-Signature value).
  • Reject any delivery where decryption fails or the signature is malformed.

Example Request

curl
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

Try it

Requests are sent without cookies — you must provide a valid Bearer <api_key>:<api_secret> header.

API Response
Click the Send API Request button above and see the response here!

Example Response

json
{
  "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"
}