Fieldwork
Collect recordings and images from adults, with consent recorded per participant and per item, reviewed by your team and delivered as versioned datasets. Participants open a branded page on their phone; no account or app needed.
How it works
- Create a project in the console or through the API, then set who can take part, the policy and consent, branding, the data type, prompts and rewards.
- The account holder publishes the project in the console, where they accept the acceptable use policy. The API and agents can prepare a project but not publish it.
- Participants open
/f/<project>, confirm they are 18 or over, read the notice and give each consent separately, then record. - Your team reviews each item. Accepted items go into a dataset in Datasets with their consent record and provenance.
- A participant can withdraw at any time; their items leave new versions and you see a delete-by date in the console and the API.
Keys and scopes
Calls use a Platform API key holding fieldwork:read or fieldwork:write. Add the scopes on the Keys page. The API lives on https://api.tenzro.xyz/fieldwork without a version prefix.
Create a project
curl https://api.tenzro.xyz/fieldwork/projects \
-H "Authorization: Bearer $TENZRO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Kitchen commands"}'Collect on your own page
A project can collect through your own web page or app instead of the hosted /f/<project> page. Create an ingest key (tzi_…) for the project in the console, or with fieldwork:write. It belongs to one project, works only from the origins you list (https, or http on localhost; no wildcards), and can only run the participant flow, so it is safe to put in the page. It cannot read recordings or change the project. Revoking it stops the next request.
curl -X POST https://api.tenzro.xyz/fieldwork/projects/fwproj_…/ingest-keys \
-H "Authorization: Bearer $TENZRO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Sidekick web page", "allowed_origins": ["https://sidekick.example"]}'Your page calls https://api.tenzro.xyz/ingest directly from the participant's browser, with Authorization: Bearer tzi_…. The same rules as the hosted page apply: the page check, the age and place gate, the consent text, the quality checks and withdrawal. Show the notice and consent options that /ingest/project returns, and send the participant's choices. Keep the participant token in the page; it is the participant's session. Send X-Device-Id (a random id the page keeps) so limits count the person's device, not your server.
| Method | Path | What it does |
|---|---|---|
GET | /ingest/project | Name, data type, prompts, capture format and limits, questions, notice and consent options, endpoint list |
GET | /ingest/challenge | A proof of work to solve before starting a participant |
POST | /ingest/participants | Start a participant: country, dob, answers, challenge, nonce. Returns participant_token |
GET | /ingest/participant | The participant's state, consent and prompts (X-Participant-Token) |
POST | /ingest/participant/consent | Record consent to the current text: {given: {scope: true}} |
POST | /ingest/items?prompt=<id> | Send one take: the WAV is the body, with X-Content-Sha256 and optional X-Fieldwork-Meta (base64url JSON) |
GET | /ingest/items | The participant's takes and their review status |
POST | /ingest/participant/withdraw | Withdraw the participant and delete their recordings |
const API = "https://api.tenzro.xyz/ingest";
const KEY = "tzi_…"; // the ingest key: safe in the page
const H = { Authorization: `Bearer ${KEY}` };
const project = await (await fetch(`${API}/project`, { headers: H })).json();
// 1. Page check: find a nonce so sha256(challenge + ":" + nonce) has `bits` leading zero bits.
const { challenge, bits } = await (await fetch(`${API}/challenge`, { headers: H })).json();
async function solve() {
for (let n = 0; ; n++) {
const h = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(`${challenge}:${n}`)));
let zeros = 0;
for (const b of h) { if (b === 0) { zeros += 8; continue; } zeros += Math.clz32(b) - 24; break; }
if (zeros >= bits) return String(n);
}
}
// 2. Start a participant (only after showing project.notice and project.consent).
const start = await fetch(`${API}/participants`, {
method: "POST",
headers: { ...H, "Content-Type": "application/json" },
body: JSON.stringify({ country: "SG", dob: "1990-04-02", challenge, nonce: await solve() }),
});
const { participant_token } = await start.json();
const P = { ...H, "X-Participant-Token": participant_token };
// 3. Consent, then one take per prompt (wav = a 16-bit mono PCM WAV as an ArrayBuffer).
await fetch(`${API}/participant/consent`, {
method: "POST",
headers: { ...P, "Content-Type": "application/json" },
body: JSON.stringify({ given: { purpose: true, training: true } }),
});
const sha = [...new Uint8Array(await crypto.subtle.digest("SHA-256", wav))].map((b) => b.toString(16).padStart(2, "0")).join("");
const res = await fetch(`${API}/items?prompt=${project.prompts[0].id}`, {
method: "POST",
headers: { ...P, "Content-Type": "audio/wav", "X-Content-Sha256": sha },
body: wav,
});
console.log(await res.json()); // { item_id, status, problems, qc }Which law applies
A project is held to the law of the places involved: where its controller is and where participants can be. It is not assumed to be an EU project. A project run from Singapore for people in Singapore is under Singapore's PDPA; the participant notice names that regulator, and an export out of Singapore needs a contract or similar route that gives comparable protection. The EU transfer mechanisms (adequacy, Data Privacy Framework, standard contractual clauses, UK agreement) are asked for only when the EU/EEA, UK or Switzerland is involved. China is its own case: participants give a separate consent to a transfer out of China, and exports need the China standard contract, a security assessment or certification.
The editor lists the laws that apply to your choices and whether an export to the destination needs a mechanism. GET /fieldwork/policy lists the law and regulator per country. Every entry is pending legal review: have counsel check it for your project. The Platform stores recordings in the EU, and the notice says so; for participants elsewhere that is itself a transfer you are responsible for.
Review
curl "https://api.tenzro.xyz/fieldwork/projects/fwproj_…/queue" -H "Authorization: Bearer $TENZRO_API_KEY"
curl https://api.tenzro.xyz/fieldwork/items/fwi_…/review \
-H "Authorization: Bearer $TENZRO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"decision": "accept"}'Export to a dataset
curl -X POST https://api.tenzro.xyz/fieldwork/projects/fwproj_…/export -H "Authorization: Bearer $TENZRO_API_KEY"Endpoints
| Method | Path | What it does | Scope |
|---|---|---|---|
GET | /fieldwork/policy | The rules every project is held to, the reason codes, and the acceptable use policy | public |
POST | /fieldwork/preview | Check settings without saving: errors, warnings and the participant notice they produce | public |
GET | /fieldwork/projects | List projects | fieldwork:read |
POST | /fieldwork/projects | Create a draft project (settings as in the console) | fieldwork:write |
GET | /fieldwork/projects/{id} | Get a project | fieldwork:read |
PATCH | /fieldwork/projects/{id} | Change settings. A live project takes only settings that pass every check; consent changes start a new consent version | fieldwork:write |
POST | /fieldwork/projects/{id}/publish | Publishing is done by the account holder in the console, where they accept the acceptable use policy | fieldwork:write |
POST | /fieldwork/projects/{id}/pause | Stop taking recordings for now | fieldwork:write |
POST | /fieldwork/projects/{id}/resume | Take recordings again | fieldwork:write |
POST | /fieldwork/projects/{id}/close | Close for good | fieldwork:write |
DELETE | /fieldwork/projects/{id} | Delete a project that was never published | fieldwork:write |
GET | /fieldwork/projects/{id}/stats | Items by status and participants | fieldwork:read |
GET | /fieldwork/projects/{id}/coverage | Accepted items against each quota | fieldwork:read |
GET | /fieldwork/projects/{id}/items | Items, newest first; ?status=pending|accepted|rejected|disputed|auto_rejected|withdrawn&limit=&before= | fieldwork:read |
GET | /fieldwork/projects/{id}/queue | The next items for this key to review (items it already voted on are left out; gold items are mixed in) | fieldwork:read |
POST | /fieldwork/items/{id}/review | Vote on an item: {decision: accept|reject, reason (required to reject), note} | fieldwork:write |
POST | /fieldwork/items/{id}/gold | Make a decided item a gold item with its known answer ({expect: accept|reject|null}) | fieldwork:write |
GET | /fieldwork/items/{id}/audio | The item's audio as WAV (byte ranges supported); ?copy=train for the 16 kHz copy | fieldwork:read |
GET | /fieldwork/items/{id}/file | The item's file as it was collected: audio, photo, video or readings (byte ranges supported) | fieldwork:read |
POST | /fieldwork/projects/{id}/export | Cut a dataset version of the accepted items: {dest_country, mechanism}; leaving the EU/EEA needs a transfer mechanism | fieldwork:write |
GET | /fieldwork/projects/{id}/exports | Export log: who, when, where, under which mechanism | fieldwork:read |
GET | /fieldwork/projects/{id}/withdrawals | Withdrawn items to delete from your copies within 30 days and exclude from future training | fieldwork:read |
GET | /fieldwork/projects/{id}/ingest-keys | Ingest keys for this project (browser-safe keys for the project's own page; secrets are never listed) | fieldwork:read |
POST | /fieldwork/projects/{id}/ingest-keys | Create an ingest key {name, allowed_origins}: bound to this project and those origins, can only send recordings through /ingest. The secret is shown once | fieldwork:write |
PATCH | /fieldwork/projects/{id}/ingest-keys/{keyId} | Change the origins a key works from: {allowed_origins} | fieldwork:write |
DELETE | /fieldwork/projects/{id}/ingest-keys/{keyId} | Revoke an ingest key; it stops on its next request | fieldwork:write |
GET | /fieldwork/projects/{id}/payouts | Participant rewards (coming soon) | fieldwork:read |
GET | /fieldwork/projects/{id}/reviewers | Votes and gold agreement per reviewer | fieldwork:read |
From agents
The same functions are tools on the MCP server. Publishing stays with the account holder.
Pricing
| What | Price | Free each month | With a plan | One-time trial |
|---|---|---|---|---|
Published project Drafts are free; participant rewards are your own budget, separate from fees. | 5,000 credits per published project per month | 1 | 3 to 10 | — |
Accepted item Charged when an item is accepted; rejected items are free. | 25 credits per accepted item | 100 | 2,000 to 10,000 | — |
Amounts are in each row's unit. Included amounts are used first, then credits.