FieldBas Partner API — Integration Guide
For companies that want to push jobs into FieldBas and receive results back on their own servers.
| Base URL (production) | https://api.fieldbas.com/api/v1 |
| Base URL (sandbox) | https://sandbox.fieldbas.com/api/v1 |
| Interactive reference | /api/docs/ (Swagger UI) · /api/redoc/ |
| Machine-readable schema | /api/schema/ (OpenAPI 3) |
| Auth | API key (Authorization: Api-Key <key>) |
| Content type | application/json |
All money is an integer in the minor unit (kobo). All timestamps are ISO-8601 UTC. All ids are UUIDs.
1. Authentication
Your FieldBas account manager (or an org Owner/Admin via Settings → API keys) issues you a key. It is shown once at creation.
Authorization: Api-Key fb_live_9c1f… ← preferred
X-API-Key: fb_live_9c1f… ← equivalent
A key is bound to exactly one organization; every request is automatically scoped to it (you never send an org id). Keys carry scopes:
| Scope | Grants |
|---|---|
jobs:read | list/read jobs, payments, ledger, audit |
jobs:write | create jobs |
jobs:review | approve / reject / transition jobs |
campaigns:write | manage campaigns & templates |
webhooks:manage | manage webhook endpoints |
A key with no explicit scopes gets jobs:read, jobs:write, webhooks:manage.
Errors follow one envelope:
{ "code": "PERMISSION_DENIED", "message": "job.create required.", "requestId": "req_01H.." }
401 invalid/expired/revoked key · 403 key lacks the scope · 409 illegal state transition · 422 business rule failed · 429 rate-limited.
2. Create jobs
A job is always created against a job template — the reusable definition of one kind of physical work: its service category, the on-site form the worker fills in, the evidence required, who is eligible to do it, the price, and the SLA. List the templates available to you (the platform's global set plus any your account manager built for you):
GET /api/v1/job-templates/
{
"id": "0b0d…",
"code": "vehicle_inspection",
"name": "Vehicle inspection",
"category": "INSPECTION", // VERIFICATION | INSPECTION | COLLECTION | SURVEY | DEPLOYMENT | OPERATIONS
"category_label": "Inspection",
"description": "Inspect a vehicle and document its condition…",
"default_payout_minor": 450000,
"min_payout_minor": 250000,
"max_payout_minor": 1500000,
"default_radius_meters": 100,
"sla_complete_hours": 72, // used to set a deadline when you don't give one
"requires_subject_authorization": false, // records-access work needs the data subject's signed consent as evidence
"form_fields": [ // the worker's on-site form
{ "key": "plate", "label": "Plate number", "type": "text", "required": true },
{ "key": "condition", "label": "Overall condition", "type": "select",
"required": true, "options": ["Excellent","Good","Fair","Poor"] }
],
"evidence_requirements": { "min_items": 6, "types": ["PHOTO"], "per_type": { "PHOTO": 6 } },
"required_skills_detail": [ { "code": "VEHICLE_INSPECTION", "name": "Vehicle inspection" } ],
"required_qualifications_detail": [ { "code": "MECHANIC_ID", "name": "Mechanic ID card", "authority": "…" } ]
}
- Specialist work is gated. If a template lists
required_qualifications_detail,
only field workers who hold that qualification verified (e.g. a mechanic ID card for vehicle inspection) are ever offered the job. Nothing changes in how you create it — matching handles eligibility.
form_fields[].type∈ `text | textarea | number | boolean | select |
multiselect | date | rating. The worker's answers come back to you in the signed verification result (see §2.5) and in job.completed`.
2.1 One job
POST /api/v1/jobs/
{
"template": "0b0d…",
"title": "Verify 12 Allen Avenue, Ikeja",
"external_reference": "ACME-INV-10231",
"instructions": "Confirm the shop exists and photograph the signage.",
"status": "OPEN",
"priority": 5,
"payout_amount_minor": 500000,
"currency": "NGN",
"target_location": { "type": "Point", "coordinates": [3.3515, 6.6018] },
"target_address": "12 Allen Ave, Ikeja, Lagos",
"target_radius_meters": 120,
"deadline": "2026-09-10T17:00:00Z",
"contact_name": "Mrs. Adeyemi",
"contact_phone": "+2348031112222"
}
external_referenceis your id. It is unique per organization and is
echoed in every webhook, so use it to correlate.
status: omit (or"DRAFT") to stage the job;"OPEN"publishes it for
assignment immediately.
target_locationis GeoJSON[longitude, latitude].payout_amount_minoris optional — omit it and the job uses the price set
for that job category (the template's default). A per-category minimum and maximum, if configured, bound what you can set. GET /api/v1/pricing/quote?payout_amount_minor=500000 returns the commission and the net amount the partner receives.
target_radius_metersanddeadlineare optional — omitted, they fall
back to the template's default_radius_meters and sla_complete_hours.
contact_nameandcontact_phoneare optional — the on-site person the
field agent should call (e.g. the individual being verified). contact_phone is normalised to E.164. Both are private: they are returned to your organization and only to the field agent after they accept the job, never in the open-job feed.
- The agent's own delivery deadline is shorter than
deadline: once a job is
accepted it must be completed within MATCH_EXECUTION_SLA_HOURS (24h by default). A job left unstarted past that window is automatically taken back from the agent, reopened, and offered to someone else — external_reference and all other fields are preserved, and you receive the usual status webhooks.
201 Created returns the job, including its FieldBas id, plus template_category and (for specialist templates) required_qualifications.
2.2 Many jobs
POST /api/v1/jobs/bulk_upload/
{
"template_id": "0b0d…",
"campaign_id": null,
"publish": true,
"jobs": [
{ "external_reference": "ACME-1", "title": "…", "payout_amount_minor": 500000,
"latitude": 6.6018, "longitude": 3.3515, "target_address": "…" },
{ "external_reference": "ACME-2", "title": "…", "payout_amount_minor": 500000,
"latitude": 6.5244, "longitude": 3.3792, "target_address": "…" }
]
}
Up to 1000 rows. publish: true creates them OPEN. Each row accepts external_reference, title, instructions, payout_amount_minor, latitude + longitude or target_address, target_radius_meters, deadline, priority, contact_name, contact_phone, state, lga, ward, and template_id (when the top-level one is omitted). Response is per-row:
{ "total": 2, "successful": 2, "failed": 0, "created_jobs": ["…","…"], "errors": [] }
File upload. Instead of the JSON jobs list you may POST the same endpoint as multipart/form-data with a file field holding a .csv or .xlsx (Excel) spreadsheet, plus form fields template_id, campaign_id and publish. The first row is a header; column names are matched case-insensitively and common aliases are accepted:
| Column | Aliases |
|---|---|
external_reference | reference, ref |
title | name |
instructions | notes |
payout_naira | payout, amount — naira, converted to payout_amount_minor |
payout_amount_minor | — |
latitude / longitude | lat / lng, lon |
target_address | address |
contact_name | contact |
contact_phone | phone |
state / lga / ward | — |
template_code | template, category — resolved against your usable templates |
deadline | due |
priority | — |
An unreadable or malformed file returns 400 { "code": "BAD_FILE", "message": … }.
The no-code console (Jobs → New job → Bulk import) wraps this: upload a .csv / .xlsx (or paste CSV text), pick the template, and it POSTs for you.
2.3 Track a job
GET /api/v1/jobs/?status=APPROVED&ordering=-created_at
GET /api/v1/jobs/{id}/
GET /api/v1/jobs/{id}/assignments/
GET /api/v1/jobs/{id}/reviews/ (via /reviews/?job={id})
Job lifecycle: DRAFT → OPEN → ASSIGNED → IN_PROGRESS → SUBMITTED → UNDER_REVIEW → APPROVED → PAID (plus REJECTED, CANCELLED, DISPUTED).
Auto-confirmed jobs
When a job is published, FieldBas checks whether the same kind of work at the same place has already been verified by an agent recently. If so the job jumps straight to APPROVED without dispatching anyone — job.completed fires within seconds and auto_verified is true. GET /api/v1/jobs/{id}/ then carries a verification block with the original check's result and evidence pointers:
"verification": {
"auto_verified": true,
"source_verification_id": "…",
"source_job_id": "…",
"verified_at": "2026-07-30T09:12:00Z",
"address_text": "12 Allen Avenue, Ikeja",
"result": { "...": "the earlier agent's structured answers" },
"evidence": [ { "type": "PHOTO", "object_key": "…", "sha256": "…" } ]
}
Matching is by job template + proximity (default 150 m) and normalised address. Ask your account manager to widen/disable this per template or organization. To cancel a job you published (needs jobs:review):
POST /api/v1/jobs/{id}/update_status
{ "status": "CANCELLED", "reason": "Client withdrew" }
2.4 Look up a prior verification (no job created)
Check whether a matching result already exists — instant, and a fraction of a live dispatch. Nothing is dispatched; a VerificationLookup is logged for billing.
GET /api/v1/verifications/lookup?type=addr_verify&latitude=6.6018&longitude=3.3515&address=12+Allen+Ave
{ "available": true, "price_minor": 15000, "currency": "NGN",
"verified_at": "2026-07-30T09:12:00Z",
"result": { "...": "the earlier agent's answers" },
"evidence": [ { "type": "PHOTO", "sha256": "…" } ],
"report": { "...": "full signed verification report" } }
A miss returns { "available": false } — create a job to dispatch an agent.
2.5 Signed verification report
Every APPROVED (or PAID) job has a self-contained, Ed25519-signed record — what compliance files. Its result object is the field worker's structured answers to the template's on-site form ({ form_field_key: value }), and evidence lists every captured photo / video / signature / document with its sha256. The worker cannot submit until every required form field is answered and every required evidence type is present.
GET /api/v1/jobs/{id}/report/ # JSON (default)
GET /api/v1/jobs/{id}/report/?as=html # printable
Verify it: take the response, remove the signature block, re-encode the rest as canonical JSON (sorted keys, no whitespace), and check the Ed25519 signature against the key at GET /api/v1/.well-known/fieldbas-signing-key.
2.6 Escrow (optional)
If escrow is enabled for your account, publishing a job places a HELD amount against your organization wallet (top up with your account manager). Approval releases it into the payout; cancellation or rejection refunds it. Publishing returns 400 ESCROW_UNFUNDED if the wallet can't cover the payout.
3. Receive results (webhooks)
Rather than polling, register an HTTPS endpoint and FieldBas will POST events to it.
POST /api/v1/webhook-endpoints/
{
"url": "https://hooks.acme.com/fieldbas",
"description": "Prod results sink",
"events": ["job.completed", "review.submitted", "payment.completed"]
}
The response includes a signing secret (api_key) — shown once. Store it.
Rotate it with POST /api/v1/webhook-endpoints/{id}/rotate_key/, send yourself a sample with POST /api/v1/webhook-endpoints/{id}/test/ {"event":"job.completed"}, and inspect attempts at GET /api/v1/webhook-deliveries/?endpoint={id}.
3.1 Events
| Event | Fires when |
|---|---|
job.created | a job is created |
job.updated | a job changes status (not a completion state) |
job.completed | a job reaches APPROVED or PAID |
assignment.accepted | a field partner accepts the job |
assignment.rejected | a field partner declines the job |
review.submitted | a reviewer approves / rejects / requests rework |
payment.completed | the partner payout settles |
3.2 Delivery format
POST https://hooks.acme.com/fieldbas
Content-Type: application/json
X-FieldBas-Event: job.completed
X-FieldBas-Delivery: 8f2c… (unique per attempt-set; dedupe on this)
X-FieldBas-Timestamp: 1756450800
X-FieldBas-Signature: sha256=1b2c3d…
{
"event": "job.completed",
"created_at": "2026-08-29T08:20:00Z",
"organization_id": "3a1e…",
"data": {
"id": "b7d9…",
"external_reference": "ACME-INV-10231",
"status": "APPROVED",
"title": "Verify 12 Allen Avenue, Ikeja",
"campaign_id": null,
"template_id": "0b0d…",
"payout_amount_minor": 500000,
"currency": "NGN",
"created_at": "2026-08-20T10:00:00Z",
"updated_at": "2026-08-29T08:20:00Z"
}
}
assignment.* wraps the same job under data.job; payment.completed adds data.reference and data.amount_minor; review.submitted adds data.decision and data.reason.
3.3 Verify the signature
X-FieldBas-Signature is sha256= + HMAC-SHA256 of the raw request body using your endpoint's signing secret.
import hashlib, hmac
def valid(body: bytes, header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header)
const crypto = require("crypto");
function valid(rawBody, header, secret) {
const expected = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(header));
}
3.4 Delivery semantics
- Respond
2xxwithin 30 s (configurable per endpoint). Anything else is a
failure.
- Retries:
408/429/5xxand network errors retry with exponential backoff
(1m, 2m, 4m…) up to retry_count attempts (default 3). Other 4xx are permanent.
- At-least-once. The same event can arrive twice — dedupe on
X-FieldBas-Delivery.
- An endpoint whose last 20 deliveries all failed is auto-suspended
(status: FAILED); re-enable it with PATCH .../{id}/ {"status":"ACTIVE"}.
- No ordering guarantee — trust
data.status, not arrival order.
4. Callback URL alternative
If you cannot host a webhook receiver, poll:
GET /api/v1/jobs/?status=APPROVED&ordering=-updated_at&page_size=100
List endpoints are paginated: { "count", "next", "previous", "results": [...] }.
5. Rate limits
Per API key: rate_limit_per_hour (default 10 000). 429 responses carry Retry-After. Bulk endpoints count as one request.
6. For AI agents (MCP)
Run the FieldBas MCP server (ships in the backend repo, mcp_server.py) and point your agent at it. Set FIELDBAS_API_KEY for the process, or pass api_key per call.
FIELDBAS_API_KEY=fb_live_… python mcp_server.py # stdio
FIELDBAS_API_KEY=fb_live_… python mcp_server.py --http # streamable HTTP :8931
Tools: create_verification, get_verification (includes the signed report once approved), lookup_prior_verification, list_verifications.
7. SDKs
A single-file Python client is in the repo at sdk/python/fieldbas.py. For other languages, generate one from /api/schema/ with openapi-generator — see sdk/README.md.
8. Quickstart
KEY=fb_live_…
BASE=https://sandbox.fieldbas.com/api/v1
# 1. pick a template
curl -s -H "Authorization: Api-Key $KEY" $BASE/job-templates/ | jq '.results[0].id'
# 2. subscribe a webhook
curl -s -X POST -H "Authorization: Api-Key $KEY" -H 'Content-Type: application/json' \
-d '{"url":"https://hooks.acme.com/fieldbas","events":["job.completed","payment.completed"]}' \
$BASE/webhook-endpoints/ | jq '{id, secret: .api_key}'
# 3. push a job
curl -s -X POST -H "Authorization: Api-Key $KEY" -H 'Content-Type: application/json' -d '{
"template":"<TEMPLATE_ID>","title":"Verify shop","external_reference":"ACME-1",
"status":"OPEN","payout_amount_minor":500000,"currency":"NGN",
"target_location":{"type":"Point","coordinates":[3.3515,6.6018]},
"target_radius_meters":120
}' $BASE/jobs/ | jq '{id, status}'
# 4. wait for job.completed on your endpoint, or poll:
curl -s -H "Authorization: Api-Key $KEY" "$BASE/jobs/?status=APPROVED" | jq '.results[].external_reference'