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: Basic header?
  • 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_digest on bytes (encode both sides; str raises on non-ASCII).
  • Return 401 for a missing, malformed or mismatched header, 200 once 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:

FieldMeaning
_idSubmission ID
_uuidSubmission UUID (use with _id to deduplicate retries)
_submission_timeWhen the submission was received
_xform_id_stringForm ID
question fieldsYour 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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 5, 2026

View on GitHub →