Pull the results of your data-collection jobs straight into your pipeline: every accepted set with its media files, SHA-256 checksums, participant metadata and signed consents. Incremental, resumable and verifiable — no archives to wait for.
Participants record and upload their sets on Wadata. Every file is probed on arrival against the media rules of your job, each participant signs the data release themselves, and two independent reviewers must agree before a set is accepted. Only accepted sets are delivered through this API.
Collected
Participants upload one file per slot; resolution, frame rate and duration are checked automatically.
Consented
Each participant signs the release for your job; a snapshot of the text is stored with the signature.
Accepted
Reviewers reach consensus. The set becomes downloadable and participants are paid.
Delivered
You poll for new accepted sets, download the files and verify each SHA-256.
https://wadata.africa/api/v1/partner
— all paths below are relative to it.
Responses are JSON (UTF-8); timestamps are ISO 8601 in UTC.
Create an API key in your Wadata account under Data API (/client/api). The key is shown once — store it in a secrets manager. Send it on every request:
Authorization: Bearer wadata_client_3f9c…
Accept: application/json
collections:read
scope: they can read your own collections and nothing else.
401.
Three requests take you from a key to verified files on disk.
export WADATA_KEY=wadata_client_…
# 1. Your collections
curl -H "Authorization: Bearer $WADATA_KEY" https://wadata.africa/api/v1/partner/collections
# 2. Accepted sets of collection 25 (participants, consents, files + sha256)
curl -H "Authorization: Bearer $WADATA_KEY" \
"https://wadata.africa/api/v1/partner/collections/25/sets?status=accepted&per_page=100"
# 3. Download a file (resumable) and verify it
curl -C - -O -J -H "Authorization: Bearer $WADATA_KEY" https://wadata.africa/api/v1/partner/files/108
sha256sum set_16_slot_01.mp4
/me
Returns the client account the key belongs to. Handy as a connectivity check.
{
"client": { "id": 1423, "name": "Roman Kutsev", "email": "crowd@unidata.pro" },
"api_key": { "name": "Production ingest", "prefix": "wadata_client_3f9c", "scopes": ["collections:read"] }
}
/collections
Every data-collection job owned by your account, newest first.
{
"collections": [{
"id": 25,
"title": "Unidata — Webcam Pairs Dataset (Batch 3)",
"status": "active",
"price_per_set": "1300",
"currency": "KES",
"deadline": null,
"created_at": "2026-09-13T14:28:19Z"
}]
}
/collections/:id
The specification (slots, media rules, consent version), quota fill per demographic group, and how many sets are in each state.
{
"collection": {
"id": 25, "title": "…", "status": "active",
"spec": {
"modality": "video",
"participants_per_set": 2,
"slots": [{ "index": 1, "key": "high_five_handshake", "title": "High-Five and Handshake", "description": "…" }, …],
"media_rules": { "min_width": 1280, "min_height": 720, "min_fps": 20, "min_duration": 8, "max_duration": 30, "formats": ["mp4","mov","webm","matroska","avi"] },
"participant_fields": ["age","gender","ethnicity"],
"quota_field": "ethnicity",
"consent_version": "1",
"recruitment_status": "open"
},
"quotas": [
{ "group": "ethnicity", "value": "brown", "label": "Brown", "target": 150, "accepted": 1, "reserved": 0, "available": 149, "status": "open" },
…
],
"sets": { "accepted": 1, "in_review": 3, "in_progress": 12 }
}
}
/collections/:id/sets
Paginated sets with participants, consent records and files. By default only accepted sets are returned, ordered by acceptance time (oldest first) so a page walk is stable while new sets arrive.
| Query parameter | Type | Description |
|---|---|---|
| status | string | Set status to list. Default accepted. Other values (in_review, rework, rejected…) return metadata only — files are never served for them. |
| updated_since | ISO 8601 | Only sets changed after this instant, e.g. 2026-09-13T00:00:00Z. Use for incremental syncs. |
| page | integer | 1-based page number. Default 1. |
| per_page | integer | Items per page, 1–200. Default 50. |
curl -H "Authorization: Bearer $WADATA_KEY" \
"https://wadata.africa/api/v1/partner/collections/25/sets?status=accepted&page=1&per_page=100&updated_since=2026-09-13T00:00:00Z"
{
"sets": [{
"id": 16,
"status": "accepted",
"submitted_at": "2026-09-13T15:18:02Z",
"accepted_at": "2026-09-13T15:21:46Z",
"updated_at": "2026-09-13T15:21:46Z",
"is_gold": false,
"participants": [
{
"id": 27, "role": "A", "participant_ref": "P27",
"age": 29, "gender": "female", "ethnicity": "black", "skin_tone": null,
"quota_group": { "group": "ethnicity", "value": "black", "label": "Black" },
"consent": { "id": 21, "version": "1", "signed_at": "2026-09-13T15:14:35Z",
"signer_name": "Dorine Otieno", "url": "https://wadata.africa/api/v1/partner/consents/21" }
},
{ "id": 28, "role": "B", "participant_ref": "P28", "age": 31, "gender": "male", "ethnicity": "brown", … }
],
"files": [
{
"id": 108, "slot_index": 1, "slot_key": "high_five_handshake",
"filename": "set_16_slot_01.mp4", "content_type": "video/mp4", "size_bytes": 150997,
"sha256": "33e49805c3da38c0ea9b719c7fd714a54c61286b156a93b3641230a4d0d74945",
"media": { "width": 1280, "height": 720, "fps": 30.0, "duration": 10.0, "codec": "h264" },
"check_status": "passed",
"url": "https://wadata.africa/api/v1/partner/files/108"
},
… 14 more slots …
]
}],
"page": 1, "per_page": 100, "total": 1, "has_more": false
}
| Set field | Type | Description |
|---|---|---|
| participants[].role | string | A is the set lead; the same people appear in every file of the set. |
| participants[].participant_ref | string | Stable pseudonymous identifier — use it in your dataset instead of names. |
| participants[].quota_group | object | The demographic group the participant counted towards. |
| files[].slot_index / slot_key | int / string | Which scripted scenario the file is; matches spec.slots. |
| files[].sha256 | hex string | SHA-256 of the file body. Verify every download against it. |
| files[].media | object | Probe results at upload (dimensions, fps, duration, codec). |
| files[].url | URL | Download endpoint for the file (see GET /files/:id). |
| is_gold | boolean | Set is a gold-standard reference used to calibrate reviewers. |
/collections/:id/sets/:set_id
The same shape as one entry of the feed, wrapped in { "set": … }. Returns
404
if the set is not part of the collection.
/files/:id
Streams the file body. Only files of accepted
sets in your own collections are served; anything else is 404.
| Response header | Type | Description |
|---|---|---|
| Content-Type | e.g. video/mp4 | As uploaded. |
| Content-Disposition | attachment | Stable name: set_<set id>_slot_<nn>.<ext>, e.g. set_16_slot_01.mp4. |
| ETag | "<sha256>" | The file's SHA-256, same as files[].sha256. |
| Accept-Ranges | bytes | Range requests are supported for resuming. |
| Content-Range | bytes a-b/total | On 206 responses. |
# curl resumes from the current size of the local file
curl -C - -O -J -H "Authorization: Bearer $WADATA_KEY" https://wadata.africa/api/v1/partner/files/108
# or ask for a byte range explicitly
curl -H "Authorization: Bearer $WADATA_KEY" -H "Range: bytes=100000-" https://wadata.africa/api/v1/partner/files/108 -o tail.bin
# → 206 Partial Content, Content-Range: bytes 100000-150996/150997
An unsatisfiable range returns 416
with Content-Range: bytes */total.
Downloads are not rate-limited, but please keep to a handful of parallel streams.
/consents/:id
The consent record as JSON, including the exact text the participant agreed to (a snapshot taken at signing time). Keep it with the set for your compliance records.
{
"consent": {
"id": 21,
"set_id": 16,
"participant": { "id": 27, "role": "A", "full_name": "Dorine Otieno" },
"version": "1",
"signer_name": "Dorine Otieno",
"signed_at": "2026-09-13T15:14:35Z",
"text": "DATA RELEASE AND CONSENT\n\nI confirm that I am 18 years of age or older…"
}
}
Sets keep arriving for the length of the collection window. The recommended loop:
updated_at
you have processed (start with nothing).
/collections/:id/sets?status=accepted&updated_since=<that value>, walking pages while
has_more
is true.
metadata.json, download files whose local SHA-256 does not match, then fetch consents.
Every file carries a SHA-256 computed on the server the moment it was uploaded. Compare it after each download (and again before you ship a dataset) — a mismatch means a truncated or altered transfer; simply download again.
Set and participant payloads contain only the demographic fields your job asked for and a stable participant_ref. Real names appear only inside consent records — store those separately from the dataset.
A file is downloadable only after the whole set passed automatic checks, both consents were signed and reviewers agreed. Rejected or reworked material never reaches the API.
Key creation and revocation are logged, and every key shows its last use and request count on your Data API page, so unexpected activity is easy to spot.
Errors are JSON objects with a single error
message.
| Status | Type | Description |
|---|---|---|
| 401 | Unauthorized | Missing, unknown, expired or revoked key. |
| 404 | Not found | The collection, set, file or consent does not exist, is not yours, or (for files/consents) its set is not accepted. |
| 400 | Bad request | Malformed parameter, e.g. updated_since is not ISO 8601. |
| 416 | Range not satisfiable | Requested byte range is outside the file. |
| 5xx | Server error | Retry with exponential back-off; nothing is lost. |
Python 3 with requests. Mirrors every accepted set into
./wadata_<collection>/set_<id>/
with its metadata, resumes partial downloads and verifies checksums. Safe to run repeatedly.
#!/usr/bin/env python3
"""Mirror accepted sets of a Wadata collection. Usage: WADATA_KEY=… ./sync_wadata.py 25"""
import hashlib, json, os, sys, requests
BASE = "https://wadata.africa/api/v1/partner"
KEY = os.environ["WADATA_KEY"]
JOB = int(sys.argv[1])
OUT = f"wadata_{JOB}"
H = {"Authorization": f"Bearer {KEY}"}
STATE = os.path.join(OUT, ".last_updated_at")
def sha256(path):
h = hashlib.sha256()
with open(path, "rb") as fh:
for chunk in iter(lambda: fh.read(1 << 20), b""):
h.update(chunk)
return h.hexdigest()
def download(f, dest):
if os.path.exists(dest) and sha256(dest) == f["sha256"]:
return # already have it
start = os.path.getsize(dest) if os.path.exists(dest) else 0
headers = dict(H)
if start:
headers["Range"] = f"bytes={start}-"
with requests.get(f["url"], headers=headers, stream=True, timeout=120) as r:
r.raise_for_status()
mode = "ab" if r.status_code == 206 else "wb"
with open(dest, mode) as fh:
for chunk in r.iter_content(1 << 20):
fh.write(chunk)
if sha256(dest) != f["sha256"]:
os.remove(dest)
raise RuntimeError(f"checksum mismatch for {dest}; will retry next run")
os.makedirs(OUT, exist_ok=True)
since = open(STATE).read().strip() if os.path.exists(STATE) else None
newest = since
page = 1
while True:
params = {"status": "accepted", "page": page, "per_page": 100}
if since:
params["updated_since"] = since
r = requests.get(f"{BASE}/collections/{JOB}/sets", headers=H, params=params, timeout=60)
r.raise_for_status()
body = r.json()
for s in body["sets"]:
d = os.path.join(OUT, f"set_{s['id']}")
os.makedirs(d, exist_ok=True)
with open(os.path.join(d, "metadata.json"), "w") as fh:
json.dump(s, fh, indent=2)
for f in s["files"]:
download(f, os.path.join(d, f["filename"]))
for p in s["participants"]:
if p["consent"]:
c = requests.get(p["consent"]["url"], headers=H, timeout=60).json()["consent"]
with open(os.path.join(d, f"consent_{p['role']}.json"), "w") as fh:
json.dump(c, fh, indent=2)
newest = max(newest or "", s["updated_at"])
print("synced set", s["id"])
if not body["has_more"]:
break
page += 1
if newest:
with open(STATE, "w") as fh:
fh.write(newest)
We are happy to review your ingest pipeline or add fields you need in the payload.
support@wadata.africa