For developers

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)
AuthAPI key (Authorization: Api-Key <key>)
Content typeapplication/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:

ScopeGrants
jobs:readlist/read jobs, payments, ledger, audit
jobs:writecreate jobs
jobs:reviewapprove / reject / transition jobs
campaigns:writemanage campaigns & templates
webhooks:managemanage 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_reference is 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_location is GeoJSON [longitude, latitude].
  • payout_amount_minor is 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_meters and deadline are optional — omitted, they fall

back to the template's default_radius_meters and sla_complete_hours.

  • contact_name and contact_phone are 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:

ColumnAliases
external_referencereference, ref
titlename
instructionsnotes
payout_nairapayout, amount — naira, converted to payout_amount_minor
payout_amount_minor—
latitude / longitudelat / lng, lon
target_addressaddress
contact_namecontact
contact_phonephone
state / lga / ward—
template_codetemplate, category — resolved against your usable templates
deadlinedue
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

EventFires when
job.createda job is created
job.updateda job changes status (not a completion state)
job.completeda job reaches APPROVED or PAID
assignment.accepteda field partner accepts the job
assignment.rejecteda field partner declines the job
review.submitteda reviewer approves / rejects / requests rework
payment.completedthe 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 2xx within 30 s (configurable per endpoint). Anything else is a

failure.

  • Retries: 408/429/5xx and 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'