Agent skill
KoboToolbox Webhooks Skill
Receive and authenticate KoboToolbox REST Service webhooks. Use when setting up an endpoint for KoboToolbox form submissions, verifying the HTTP Basic Authorization header, debugging 401s, or handling JSON or XML submission bodies (_id, _uuid, _submission_time, _xform_id_string).
Install this skill
npx skills add hookdeck/webhook-skills --skill kobotoolbox-webhooks
KoboToolbox (field data collection) calls its webhooks REST Services. A REST Service posts each new submission of a project to a URL you choose, as JSON or XML. Requests are not signed and have no event type: every request is a new submission (edits send nothing). Your endpoint authenticates the request with HTTP Basic auth, using the username and password you entered on the REST Service.
A static shared credential authenticates the sender, not the body, and gives no replay protection (no signature, no timestamp). Use HTTPS and rotate the credential if it leaks.
When to Use This Skill
- How do I receive KoboToolbox REST Service webhooks?
- How do I verify the KoboToolbox
Authorization: Basicheader? - Does KoboToolbox sign webhook payloads? (No: use Basic auth)
- How do I handle a KoboToolbox JSON or XML submission?
- Why does my KoboToolbox endpoint return 401?
Verification (core)
const crypto = require('crypto');
// Hash both sides: equal-length buffers, so timingSafeEqual never throws.
const safeEqual = (a, b) => crypto.timingSafeEqual(
crypto.createHash('sha256').update(a).digest(), crypto.createHash('sha256').update(b).digest());
function verifyKoboRequest(headers, user = process.env.KOBOTOOLBOX_WEBHOOK_USERNAME,
pass = process.env.KOBOTOOLBOX_WEBHOOK_PASSWORD) {
if (!user || !pass) return false; // never accept when unconfigured
const m = /^basic +([A-Za-z0-9+/]+=*)\s*$/i.exec(headers.authorization || '');
if (!m) return false;
const creds = Buffer.from(m[1], 'base64');
const i = creds.indexOf(0x3a); // split on the FIRST colon
if (i < 0) return false;
const userOk = safeEqual(creds.subarray(0, i), Buffer.from(user));
const passOk = safeEqual(creds.subarray(i + 1), Buffer.from(pass));
return userOk && passOk; // evaluate both, no short-circuit
}
- Header:
Authorization: Basic base64(username:password); scheme is case-insensitive. - Python:
hmac.compare_digeston bytes (encode both sides;strraises on non-ASCII). - Return
401for a missing, malformed or mismatched header,200once handled. - Read the body raw (
express.raw({ type: '*/*' })) and authenticate before parsing. The body may be JSON or XML; don't reject non-JSON.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Payload
There is no event type. The body is the submission:
| Field | Meaning |
|---|---|
_id | Submission ID |
_uuid | Submission UUID (use with _id to deduplicate retries) |
_submission_time | When the submission was received |
_xform_id_string | Form ID |
| question fields | Your form's answers, keyed by question name |
The REST Service can also send XML, only a subset of fields, or (JSON only) a custom wrapper around the submission. KoboToolbox documents 3 retries (after 60, 600 and 6,000 seconds), though the open-source kpi sender declares a backoff capped at 1,200 seconds and retries only 502, 503, 504 and read timeouts, so a 401 or 500 fails at once. Return 503 for a temporary problem.
Environment Variables
KOBOTOOLBOX_WEBHOOK_USERNAME=your-username
KOBOTOOLBOX_WEBHOOK_PASSWORD=your-long-random-password
Local Development
KoboToolbox can't reach localhost. Tunnel with the Hookdeck CLI (no account required):
npx hookdeck-cli listen 3000 kobotoolbox --path /webhooks/kobotoolbox
Use the printed URL as the REST Service endpoint.
Resources
- references/overview.md - What REST Services are, when they fire, payload shape
- references/setup.md - Register a REST Service and set the credentials
- references/verification.md - Basic auth check details, gotchas, debugging 401s
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests