# Hannu developer docs Source: https://docs.hannu.africa/ Build the trusted human layer into your agent. Connect over MCP or REST, lock escrow before work starts, and collect a signed Verified report on every task. --- Hannu is the trusted layer between AI agents and the real world. Your agent hires KYC-verified Nigerian **Operators** for real-world tasks — a field check, a photo, a survey, a call — and gets back a **signed Verified report**, not a screenshot. Money locks in escrow before any work starts, and Hannu never holds it: licensed partners custody funds, and our append-only ledger records every movement. You connect the same way you'd add any other tool to your agent — over **MCP** or **REST** — and the platform handles identity, matching, verification, and payout. ## Start here > **Three calls to a verified outcome** > > Estimate the work, create the task under escrow, then read the signed report. That's the whole loop — over MCP or plain HTTPS. - **[Quickstart (MCP)](/get-started/quickstart-mcp)** — add the Hannu MCP server to your agent and run your first task. - **[Quickstart (REST)](/get-started/quickstart-rest)** — from an API key to a verified outcome with `curl`. - **[Authentication](/get-started/authentication)** — how API keys work and how they're issued. ## The whole surface, at a glance One governed layer sits behind both MCP and REST — discover, create, verify, settle, and govern. Every mutating call is idempotent; every error names the exact control that failed. **Discovery** — Unauthenticated, machine-readable descriptions of the platform. - `GET /v1/openapi.json` — The OpenAPI 3.1 document. - `GET /llms.txt` — A machine-first map of the API for agents. - `GET /v1/content` — The governed task-type catalogue and display states. - `GET /v1/pricing` — Machine-readable pricing — every value tagged with its config key. - `GET /v1/status` — System status with 90-day history and uptime. - `POST /v1/pass/verify` — Verify a hannu Pass credential (public, Tier-A). **Agent & wallet** — Who you are to the platform, and the money behind your tasks. - `GET /v1/me` — The calling agent’s identity, delegation flags and kill-switch state. - `GET /v1/wallet` — Org wallet balance. - `POST /v1/wallet/fund` — The funding-rails matrix and minimum top-up (read-only — no state change). **Workers** — Search and read the public projection of eligible Operators — never PII. - `GET /v1/workers` — Search eligible Operators by zone and skill. - `GET /v1/workers/:id` — One Operator’s public projection. **Tasks** — The core loop — estimate, create under escrow, then verify and settle. - `POST /v1/estimate_task` — Dry-run pricing with zero state change. - `POST /v1/tasks` — Create and escrow-lock a task. - `GET /v1/tasks` — List the org’s tasks. - `GET /v1/tasks/:id` — Full task state, including escrow and proof. - `GET /v1/tasks/:id/proof` — The AI verification report summary. - `POST /v1/tasks/:id/approve` — Release escrow to the Operator. - `POST /v1/tasks/:id/reject` — Reject with a structured reason — opens a dispute, freezes escrow. - `POST /v1/tasks/:id/cancel` — Cancel before acceptance and refund escrow. **Workflows** — Composite outcome products — one order, one escrow lock, a signed report. - `GET /v1/workflow_products` — The flag-gated workflow catalogue for a zone. - `POST /v1/workflows` — Order a workflow under a single escrow lock. - `GET /v1/workflows/:id` — Per-step progress (no worker identities). - `GET /v1/workflows/:id/report` — The Verified report — json, markdown, html or attestation. **Campaigns** — Active campaigns — empty while the campaigns engine flag is off. - `GET /v1/campaigns` — List active campaigns. ## What makes Hannu different - **Escrow-native.** Funds lock before the task exists. No work, no risk. - **Proof, not promises.** Every outcome is verified by AI and, where needed, a human — then signed. - **Governed by construction.** Per-agent spend caps, delegation flags, and an instant kill switch are checked before any task is created. - **Safe by design.** Credential-class work — account creation, CAPTCHA, OTP handling — is refused in the write path. See [absent by design](/mcp/absent-by-design). ## Machine-readable by default These docs are built for agents as much as people. Every page is available as clean Markdown — append `.md` to any URL — and the whole corpus is published at [`/llms.txt`](/llms.txt) and [`/llms-full.txt`](/llms-full.txt). Use the **Copy page** menu at the top of any page to hand it to an assistant with one click. --- # Quickstart with MCP Source: https://docs.hannu.africa/get-started/quickstart-mcp Add the Hannu MCP server to your agent and go from a fresh key to a signed Verified report in three tool calls. --- Hannu ships a **Model Context Protocol** server, so an MCP-capable agent (Claude, Cursor, or your own) discovers the tools and calls them directly — no glue code. The MCP surface and the [REST API](/get-started/quickstart-rest) share one governance layer, so everything here has a REST equivalent. ## 1. Add the server Point your agent at the Hannu MCP endpoint and pass your API key as a bearer token. ```json // ~/.your-agent/mcp.json { "mcpServers": { "hannu": { "url": "https://mcp.hannu.africa", "auth": "Bearer ${HANNU_KEY}" } } } ``` > **Getting a key** > > API keys are issued when you register an organization. See [Authentication](/get-started/authentication). Keys look like hnu_live_… and are shown once. ## 2. Estimate, then create Ask your agent to price the work first — `estimate_task` changes no state — then create it. Creating a task locks escrow atomically and matches a verified Operator. A natural-language instruction your agent can act on: ```text Use the hannu tools. Estimate a field_verification task at 23 Commercial Avenue, Yaba, Lagos, requiring a geotagged photo. If it's under ₦3,000, create it with an idempotency key. ``` Under the hood your agent calls `estimate_task`, reads the price, then `create_task`. Escrow is locked the moment the task is created — nothing is at risk before that. ## 3. Read the outcome When proof passes verification, the task carries its verification outcome. Your agent calls `get_task` (or `get_proof`) and reads it: ```json { "id": "task_8f21", "state": "approved", "proof": { "has_proof": true, "outcome": "pass", "confidence_score": 0.97, "submitted_at": "2026-07-18T09:14:00Z" } } ``` That's the loop. Subscribe to [webhooks](/webhooks/overview) so you're notified on every state change instead of polling. ## Next steps - Browse the full [tool catalogue](/mcp/tools) — 16 tools live today. - Understand [how Hannu works](/core-concepts/how-hannu-works) end to end. - Learn why some tools are [absent by design](/mcp/absent-by-design). --- # Quickstart with REST Source: https://docs.hannu.africa/get-started/quickstart-rest From an API key to a verified outcome in three HTTPS calls — estimate, create under escrow, then read the signed report. --- The REST API is a predictable JSON API over `https://api.hannu.africa`, versioned at `/v1`. Everything the [MCP server](/get-started/quickstart-mcp) does is available here too — they share one governance layer. ## 1. Estimate the work `estimate_task` returns a price and changes no state. Send your bearer key on every call. ```bash curl -X POST https://api.hannu.africa/v1/estimate_task \ -H "Authorization: Bearer $HANNU_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "field_verification", "reward_major": "2500.00", "zone_ref": "lagos_yaba" }' ``` ## 2. Create the task — escrow locks atomically `POST /v1/tasks` funds escrow, runs the safety filter, and matches a verified Operator in one call. Mutating calls require an **`Idempotency-Key`** — a retry or dropped connection never double-creates a task or double-charges escrow. ```bash curl -X POST https://api.hannu.africa/v1/tasks \ -H "Authorization: Bearer $HANNU_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "type": "field_verification", "title": "Confirm storefront at 23 Commercial Avenue, Yaba", "instructions": "Visit 23 Commercial Avenue, Yaba, Lagos. Photograph the storefront and confirm the business is trading.", "reward_major": "2500.00", "deadline": "2026-08-01T17:00:00Z", "proof_requirements": [ "photo", "geofence" ], "zone_ref": "lagos_yaba" }' ``` Escrow is locked atomically and the task starts in `screening`, where the safety filter runs before it goes `live` for matching. ## 3. Read the outcome Poll `GET /v1/tasks/:id` — or, better, subscribe to [webhooks](/webhooks/overview) — until the task reaches `approved` (`paid` once the Operator has been settled). The task carries its verification outcome under `proof`. ```json { "data": { "id": "task_8f21", "state": "approved", "reward_minor": "250000", "currency": "NGN", "proof": { "has_proof": true, "outcome": "pass", "confidence_score": 0.97, "submitted_at": "2026-07-18T09:14:00Z" } } } ``` > **Response envelope** > > Successful responses wrap the payload in {'{ data }'}; list endpoints return a bounded {'{ data: [...] }'} array. Errors use RFC 9457 problem+json — see Errors. ## Next steps - The full [API reference](/api-reference). - [Idempotency](/core-concepts/idempotency) in depth. - Set up [webhooks](/webhooks/overview) so you stop polling. --- # Authentication Source: https://docs.hannu.africa/get-started/authentication How API keys are issued, how to send them, and how governance is enforced on every call. --- Every call to the Hannu API — REST or MCP — is authenticated with a **bearer API key** scoped to your organization's agent. ## API keys Keys are issued during organization registration, through a one-time key ceremony. A live key looks like: ```text hnu_live_9c2f… (shown once, at creation — store it in your secrets manager) ``` Keys are hashed at rest (SHA-256); Hannu never stores or can show you the raw value again. If a key is lost or leaked, revoke it and issue a new one from the console. > **Treat keys as secrets** > > A key can create tasks and move escrow within its spend policy. Keep it server-side, never in client code or a repo. Rotate on exposure. ## Sending the key Send the key as a bearer token on every request: ```http Authorization: Bearer hnu_live_9c2f… ``` For MCP, set it once in your `mcp.json` (see the [MCP quickstart](/get-started/quickstart-mcp)). The MCP server maps the bearer to the same agent context as REST. ## Governance is part of auth Authentication resolves *who* you are; governance decides *what that agent may do* — and it runs before any task is created: - **Delegation flags** — capabilities such as `can_post_tasks` must be granted to the agent. - **Spend policy** — per-agent daily and per-task caps. - **Kill switch** — an instant freeze; a frozen agent creates nothing. When a call is refused, the error names the exact control (`spend_policy_exceeded`, `delegation_denied`, `agent_frozen`) so your agent can branch on it. See [Errors](/core-concepts/errors) and [Governance & delegation](/core-concepts/governance). ## Discovery endpoints need no key A handful of endpoints are unauthenticated — the OpenAPI document, `llms.txt`, `/v1/content`, `/v1/pricing`, `/v1/status`, and `POST /v1/pass/verify`. Everything that touches your org or money requires a key. --- # How Hannu works Source: https://docs.hannu.africa/core-concepts/how-hannu-works The path a task takes from your agent to the real world and back — escrow, matching, proof, verification, and payout. --- Every task follows the same path. Your agent describes an outcome; Hannu locks the money, finds a verified human, checks the proof, and pays out — recording every step on an append-only ledger. ## The loop 1. **Estimate** — your agent prices the work with `estimate_task`. No state changes. 2. **Create** — `create_task` locks escrow, runs the safety filter, and matches an eligible Operator. Money is committed here, and only here. 3. **Accept & do the work** — a KYC-verified Operator accepts the offer (on the app or over WhatsApp) and completes the task in the real world. 4. **Prove** — the Operator submits proof: geotagged photos, a form, a recording. The task moves to `proof_submitted`. Capture timestamps are recorded server-side, not trusted from the device. 5. **Verify** — the task enters `verifying` and AI checks the proof (location, freshness, EXIF, duplicates, content match). High-confidence results auto-approve; the rest go to human review. 6. **Settle** — on approval the task reaches `approved`, escrow releases to the Operator, and it settles to `paid`. On rejection, escrow freezes and a dispute opens. > **The AI is never in the money path** > > Verification informs the decision, but escrow release and task acceptance are deterministic state transitions — an AI model is never the thing that moves money. ## Money you can trust - **Hannu never custodies funds.** Balances live at licensed partners; our double-entry ledger maps virtual balances to their real accounts. - **Money is integer minor units** — kobo for naira, cents for dollars — never floating point. - **The ledger is append-only.** Corrections are new entries, never edits. Read more in [Escrow & the ledger](/core-concepts/escrow-and-ledger). ## Proof, not promises The output of a task is a **Verified report** — a signed statement of what was checked and found, with a confidence score and the list of checks that ran. It's designed to be filed and independently verified, not eyeballed. See [Proof & verification](/core-concepts/proof-and-verification). ## Governed by construction Spend caps, delegation flags, and the kill switch are checked in the write path, before a task exists. Credential-class work is refused there too. See [Governance & delegation](/core-concepts/governance). --- # Idempotency Source: https://docs.hannu.africa/core-concepts/idempotency Every mutating call takes an Idempotency-Key so a retry or a dropped connection never double-creates a task or double-charges escrow. --- Networks fail mid-request. Hannu makes that safe: **every mutating endpoint requires an `Idempotency-Key` header**, and a replay of the same key returns the original result instead of doing the work twice. ## How to use it Generate a unique key per logical operation — a UUID is ideal — and send it on the write: ```http POST /v1/tasks Authorization: Bearer hnu_live_… Idempotency-Key: 7f3c4e21-9a02-4b8d-b1f0-2c9a51d3e4a7 Content-Type: application/json ``` If you retry with the **same key and the same body**, you get back the original response — the task is created once. Retrying is always safe. ## The rules - **Same key, same body → replayed.** The stored response is returned and a replay is flagged, so no second task is created and no second escrow lock happens. - **Same key, different body → conflict.** Reusing a key with a changed body returns `idempotency_conflict` (409). Use a fresh key for a new request. - **Replay window is 24 hours.** After that, a key may be reused for a genuinely new operation. > **One key per intent** > > Derive the key from the thing you're trying to create — for example a hash of (order id + step) — so an automatic retry reuses it, but a genuinely new task gets a new one. ## Which calls need it Every `POST` that changes state: creating tasks and workflows, approving, rejecting, and cancelling. Read-only `GET`s don't need a key — including `POST /v1/wallet/fund`, which only reads back your funding options and changes nothing. The [API reference](/api-reference) marks each idempotent endpoint. --- # Errors Source: https://docs.hannu.africa/core-concepts/errors Hannu returns RFC 9457 problem+json — a stable code, a safe detail, and a request_id — so your agent can branch on the exact reason. --- Every error is an [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) `application/problem+json` document. It names the exact control that failed with a **stable machine code**, so your agent can branch reliably instead of parsing prose. ## The shape ```json { "type": "https://docs.hannu.africa/errors/insufficient_escrow_balance", "title": "Wallet balance is below the amount this task would lock", "status": 402, "code": "insufficient_escrow_balance", "detail": { "required": "303750", "available": "120000" }, "request_id": "req_9c1" } ``` - **`code`** — the stable identifier to branch on. It never changes for a given condition. - **`title`** — the human-readable message for this occurrence. Read it for display, not for branching. - **`status`** — the HTTP status. - **`detail`** — a structured object of condition-specific fields (here, `required` and `available`), present only when the error carries specifics. Never contains PII or internal state. Read the numbers you need from it; read the human message from `title`. - **`request_id`** — quote this to support to trace any request. > **Branch on code, not status** > > Several conditions share a status (three different 403s, for example). Always switch on code. ## Handling patterns - **`rate_limited` (429)** — back off and retry after the `Retry-After` header. - **`idempotency_conflict` (409)** — you reused a key with a different body; use a fresh key. - **`insufficient_escrow_balance` (402)** — fund the wallet, then retry. - **`internal` (500)** — safe to retry; quote the `request_id` if it persists. ## The full catalogue Every code, its status, and its meaning is on the [Errors reference](/api-reference/errors-reference). --- # Escrow and the ledger Source: https://docs.hannu.africa/core-concepts/escrow-and-ledger How funds lock before a task exists, why Hannu never holds the money, and how an append-only double-entry ledger records every move. --- Money commits before work begins. When you create a task, the price is locked in escrow **before the task exists** — the write that creates the task and the lock is one atomic step. An Operator never starts on an unfunded task, and you never owe money you didn't set aside. ## Money is integers Every amount is an integer in the currency's minor unit — kobo for Naira, cents for dollars. A `reward_minor` of `250000` is ₦2,500.00. There are no floats anywhere in the money path: floats round, and rounding loses money. When you read or send an amount, you're reading or sending whole minor units. ```json { "reward_minor": "250000", "currency": "NGN" } ``` ## Hannu never holds the funds Hannu is not a custodian. Licensed partners hold the actual money; our system records who is owed what. What we keep is an **append-only double-entry ledger** — every movement is written as balanced debit and credit entries, and entries are never edited or deleted. A mistake is corrected by writing a new, reversing entry, not by changing the original. The history is the source of truth, and it stays complete. > **Why append-only** > > An append-only ledger means the record of a payment can always be reconstructed and audited. Because corrections are new entries rather than edits, there is no way to quietly rewrite what happened — every state the money passed through remains visible. ## Escrow-lock states Behind each task is one escrow lock, which moves through these states as the money settles. The lock is internal bookkeeping — it isn't serialized on the task, so you read the task's own `state` and your [wallet balance](/api-reference/wallet) rather than a lock field: - **`locked`** — funds are reserved against the task at creation. They are committed to this task and can't be spent elsewhere. - **`partial`** — part of the lock has been released — for example one slot of a multi-worker task — while the remainder stays held. - **`released`** — the task was approved and the Operator has been paid. The ledger records the transfer out of escrow. - **`disputed_frozen`** — proof was rejected and a dispute is open. Funds stay put, released to neither side, until the dispute resolves. A refund — when a task is cancelled before acceptance, or a dispute resolves in your favour — returns funds to your wallet through the ledger. It is a task and ledger outcome, not a lock state. Each transition is a ledger event, so the balance on your wallet and the state on the task always agree. ## The flow 1. You call [`POST /v1/tasks`](/api-reference/tasks). Escrow locks in the same transaction that creates the task. 2. An Operator completes the work and submits proof. 3. You [approve](/api-reference/tasks) and escrow **releases** to the Operator, or you [reject](/api-reference/tasks) and escrow **freezes** while a dispute runs. 4. Cancel before acceptance and escrow **refunds** to your wallet. The AI verification layer is never in this path — it produces a report, but releasing, freezing, or refunding money is a decision your approval drives, not the model. See [Proof and verification](/core-concepts/proof-and-verification). --- # Pagination Source: https://docs.hannu.africa/core-concepts/pagination List endpoints return bounded { data } arrays today — tasks capped at 50, workers via a limit parameter. Cursor pagination is not yet available. --- List endpoints return a bounded set of rows in a single `{ data }` array. There is no cursor to follow and no `meta` envelope today — a list call gives you the most recent rows, up to the endpoint's cap, in one response. ## The envelope A single-object response wraps the payload in `{ data }`. A list response is the same envelope with an array payload: ```json { "data": [ { "id": "task_8f21", "type": "field_verification", "state": "approved" }, { "id": "task_8f22", "type": "field_verification", "state": "matched" } ] } ``` There is no `meta` object and no `next_cursor` — the list endpoints don't emit them. ## What each list returns - **`GET /v1/tasks`** — your org's tasks, newest first, capped at 50. It takes no query parameters, so you always get the most recent page. - **`GET /v1/workers`** — the public worker projection for a zone. Pass `limit` to set how many rows come back (default 20, capped at 50), and `zone_ref` to filter by zone. ```bash curl https://api.hannu.africa/v1/workers?zone_ref=lagos_yaba&limit=20 \ -H "Authorization: Bearer $HANNU_KEY" ``` ## Cursor pagination is not yet available > **Bounded lists today** > > The list endpoints return bounded results, not paged ones — there is no cursor, next_cursor, or meta to follow yet. Cursor pagination is planned; until it ships, design around the caps above (tasks: 50 newest; workers: limit, max 50). Subscribe to webhooks for task state changes instead of polling long lists. --- # Rate limits Source: https://docs.hannu.africa/core-concepts/rate-limits Per-agent rate limits, the 429 response, and how to back off cleanly. --- Rate limits are enforced **per agent**. Each agent's key has its own budget, so one busy agent can't exhaust the limit for another. When you exceed it, the API tells you exactly how long to wait. ## When you're limited A request over the limit returns `429` with a `rate_limited` error and a `Retry-After` header giving the number of seconds to wait before trying again: ```http HTTP/1.1 429 Too Many Requests Retry-After: 2 Content-Type: application/problem+json ``` ```json { "type": "https://docs.hannu.africa/errors/rate_limited", "title": "Too many requests", "status": 429, "code": "rate_limited" } ``` ## Backing off - **Honour `Retry-After`.** It is the server's instruction — wait at least that many seconds before retrying. - **Add jitter.** When several requests hit the limit together, retrying them at the same instant just collides again. Spread retries with a small random offset. - **Use exponential back-off past the first retry.** If you're still limited after waiting, double the delay each attempt rather than hammering at a fixed interval. - **Reuse your `Idempotency-Key` on the retry.** A retried write must carry the same key so it can't double-create. See [Idempotency](/core-concepts/idempotency). > **Don** > > Immediately re-sending a `429`'d request keeps you over the limit and delays recovery. Read `Retry-After`, wait, then retry. ## Staying under the limit Prefer one request that does the work over many that poll. Page through lists with [cursors](/core-concepts/pagination) rather than re-fetching, and cache responses that don't change between calls. --- # Versioning and stability Source: https://docs.hannu.africa/core-concepts/versioning The API is versioned in the URL, additive-only within a version, with breaking changes reserved for a new version and a dual-run window. --- The API is versioned in the URL. Everything lives under `/v1`, and we hold that path to a strict compatibility promise so an integration you write today keeps working. ## Additive-only within a version Inside `/v1`, changes are **additive only**. We may add new endpoints, new optional request fields, and new fields on responses. We do not change the shape of an existing field, remove one, rename one, or change what an existing value means. Write your client to ignore fields it doesn't recognise and it won't break when we add them. ## Breaking changes get a new version A change that would break an existing caller — removing a field, changing a type, altering required inputs — goes in a new version, `/v2`, at a new URL. When that happens we **dual-run**: `/v1` and `/v2` are served side by side for a migration window, so you move over deliberately rather than all at once. ## What counts as the contract The URL surface isn't the whole promise. These are part of the versioned contract and follow the same rules: - **Tool names** — the [MCP](/mcp/connect) tool identifiers agents call. - **Event names** — the event types on webhooks and the event stream. - **Error codes** — the stable `code` on every [error](/core-concepts/errors), and the `type` URI it resolves to at `https://docs.hannu.africa/errors/`. A `code`, a tool name, or an event name won't change meaning inside a version. New ones may appear; existing ones stay put. > **Pin to the version, match on codes** > > Call the version explicitly in the path (`/v1/...`) and branch your logic on the stable `code` and event name rather than on human-readable titles or messages — titles are for people and may be reworded, codes are for machines and are held stable. --- # Governance Source: https://docs.hannu.africa/core-concepts/governance Delegation flags, spend caps, a kill switch, and a safety filter — the controls checked in the write path before a task is ever created. --- An agent acting on your behalf spends real money in the real world. Governance is the set of controls that decides, at the moment of a write, whether a given agent is allowed to do a given thing. All of it runs **in the write path** — before a task is created and before escrow locks — so a blocked action never becomes a real one. ## Delegation flags Each agent carries flags that say what it may do — for example `can_post_tasks`. An action the agent isn't delegated to take is refused with `delegation_denied` (403) before anything is created. Flags are set when you provision the agent and changed from the admin surface, not by the agent itself. ## Spend policy Every agent has a spend policy with two kinds of cap: a **daily cap** across all its tasks, and a **per-task cap** on any single task. A create that would push the agent past either cap is rejected with `spend_policy_exceeded` (403). Because the check happens before escrow locks, the money is never reserved in the first place. ## Kill switch An agent can be frozen instantly. Once frozen, every write it attempts returns `agent_frozen` (403) — no new tasks, no approvals, nothing that moves money. The switch is immediate and total; use it when an agent is misbehaving and sort out the details afterward. ## Safety filter Task content passes a safety filter in the same write path. Content that the filter blocks — a disallowed task type, a prohibited request — is refused with `safety_filter_blocked` (422). This is also where credential-class work is stopped by design: Hannu never runs tasks for account creation, CAPTCHA solving, or OTP handling, and there is no configuration that turns that off. See [MCP: absent by design](/mcp/absent-by-design). ## The error codes | Code | Status | Meaning | | --- | --- | --- | | `delegation_denied` | 403 | The agent isn't delegated for this action. | | `spend_policy_exceeded` | 403 | The action would exceed the daily or per-task cap. | | `agent_frozen` | 403 | The agent is frozen by the kill switch. | | `safety_filter_blocked` | 422 | Task content was blocked by the safety filter. | > **Checked before the money moves** > > Every one of these runs before a task is created and before escrow locks. A governance rejection means nothing happened — no task, no lock, no ledger entry. Provisioning an agent and its keys is covered in Authentication. --- # Proof and verification Source: https://docs.hannu.africa/core-concepts/proof-and-verification What proof is, the automated checks and human review that assess it, and the signed Verified report that comes out — with the AI kept out of the money path. --- Proof is the evidence an Operator submits that the work was done. Verification is how that evidence is assessed. The output is a signed report you read before you approve — and the model that produces it never moves money. ## What proof is Depending on the task type, proof is one or more of: - **Geotagged photos** — images carrying the location and time they were captured. - **Structured forms** — the fields the task asked the Operator to fill in. - **Recordings** — short audio or video where the task calls for it. Capture time is **recorded by the server** when the proof arrives, not taken on trust from the device clock, so freshness can be judged against a time we control. ## The checks Automated checks run first over the submitted proof: - **Location / geofence** — is the capture inside the target area? For a task in Yaba, Lagos (lat 6.51, lng 3.37), proof photographed elsewhere fails. - **EXIF** — does the image metadata hold together, or does it show signs of tampering or a re-saved screenshot? - **Freshness** — was the proof captured within the task's window, against the server-recorded time? - **Duplicate** — has this image or evidence been submitted before, here or on another task? - **Content match** — does what the proof shows match what the task asked for? Whatever the automated checks can't settle goes to **human review**. A person makes the call on the ambiguous cases; the automation handles the clear ones. ## The Verified report The result is a signed **Verified report** with a **confidence score** — a single artifact recording which checks ran, what they found, and where a human agreed. You read it through [`GET /v1/tasks/:id/proof`](/api-reference/tasks) and decide whether to approve or reject. > **The AI is never in the money path** > > Verification produces a report; it does not release, freeze, or refund escrow. Moving money is your approval decision, made after you read the report — the model is never in the synchronous path of a payment or a task acceptance. How escrow itself moves is covered in Escrow and the ledger. Where this sits in the overall loop is laid out in [How Hannu works](/core-concepts/how-hannu-works). --- # Create and fund a task Source: https://docs.hannu.africa/guides/create-and-fund-a-task A worked walk-through — fund your wallet, estimate the work, and create a task under escrow. --- This guide takes you from an empty wallet to a live task. It assumes you have an API key ([Authentication](/get-started/authentication)). ## 1. Check your wallet ```bash curl https://api.hannu.africa/v1/wallet \ -H "Authorization: Bearer $HANNU_KEY" ``` ```json { "data": { "balance_minor": "0", "currency": "NGN" } } ``` ## 2. Fund it `POST /v1/wallet/fund` is read-only: it returns the funding options and the minimum top-up for your org, and changes nothing — no `Idempotency-Key`, no amount to send. Follow the returned instructions to move money in; the balance updates when the deposit settles at the partner. ```bash curl -X POST https://api.hannu.africa/v1/wallet/fund \ -H "Authorization: Bearer $HANNU_KEY" ``` > **Money is integer minor units** > > Amounts are always in kobo (or cents) as integers — a balance_minor of 500000 is ₦5,000.00. Never treat one as a float. ## 3. Estimate the work ```bash curl -X POST https://api.hannu.africa/v1/estimate_task \ -H "Authorization: Bearer $HANNU_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "field_verification", "reward_major": "2500.00", "zone_ref": "lagos_yaba" }' ``` ## 4. Create it — escrow locks If the wallet holds enough, `create_task` locks escrow and enters safety screening. If it doesn't, you get `insufficient_escrow_balance` (402) — fund more and retry. ```bash curl -X POST https://api.hannu.africa/v1/tasks \ -H "Authorization: Bearer $HANNU_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "type": "field_verification", "title": "Confirm storefront at 23 Commercial Avenue, Yaba", "instructions": "Visit 23 Commercial Avenue, Yaba, Lagos. Photograph the storefront and confirm the business is trading.", "reward_major": "2500.00", "deadline": "2026-08-01T17:00:00Z", "proof_requirements": [ "photo", "geofence" ], "zone_ref": "lagos_yaba" }' ``` From here, subscribe to [webhooks](/webhooks/overview) and wait for `task.approved` — or poll `GET /v1/tasks/:id` until it reaches `approved`. --- # Estimate before you spend Source: https://docs.hannu.africa/guides/estimate-before-you-spend Price a task with estimate_task before you create it — a dry run that changes nothing and returns the exact amount escrow will lock. --- Before you create a task, price it. `estimate_task` takes the exact task shape you intend to create and returns what it would cost — with no state change, no escrow lock, and no Operator matched. Reach for it whenever your agent is about to spend. ## Why estimate first `create_task` locks escrow in the same step that creates the task. If the wallet is short, the call fails with `insufficient_escrow_balance` (402). Estimating first lets your agent decide whether the work is worth the price, and confirm the wallet holds enough, before it commits anything. The estimate is a pure read. It runs the same pricing computation `create_task` uses, so the number you get back is the number escrow will lock — for the task shape you sent. ## Ask for a price Send the type and the reward you intend to offer. The example zone throughout these docs is Yaba, Lagos (`lagos_yaba`). ```bash curl -X POST https://api.hannu.africa/v1/estimate_task \ -H "Authorization: Bearer $HANNU_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "field_verification", "reward_major": "2500.00", "zone_ref": "lagos_yaba" }' ``` `estimate_task` mutates nothing, so it needs no `Idempotency-Key`. ## Read the price The response is a `{ data }` envelope. Every amount is an integer in the currency's minor unit — kobo for Naira — split into the reward you set, the platform fee, and VAT. ```json { "data": { "reward_minor": "250000", "fee_minor": "50000", "vat_minor": "3750", "total_minor": "303750", "currency": "NGN", "time_to_match_hint": "under 30 min" } } ``` - **`total_minor`** — the total escrow will lock. `303750` is ₦3,037.50. Never treat it as a float. - **`reward_minor` / `fee_minor` / `vat_minor`** — the reward you offered, the platform fee, and VAT on the fee. `total_minor` is their sum. - **`time_to_match_hint`** — a supply signal for the zone, populated when you pass `zone_ref`. > **The numbers are governed config** > > The figures above are illustrative. Every value in the price comes from governed config, published machine-readably at GET /v1/pricing. Recompute against that endpoint rather than hardcoding any amount — a config change updates the quote, not your code. ## Then create it Once the price is acceptable and the wallet is funded, create the task with the same shape. See [Create and fund a task](/guides/create-and-fund-a-task). ```bash curl -X POST https://api.hannu.africa/v1/tasks \ -H "Authorization: Bearer $HANNU_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "type": "field_verification", "title": "Confirm storefront at 23 Commercial Avenue, Yaba", "instructions": "Visit 23 Commercial Avenue, Yaba, Lagos. Photograph the storefront and confirm the business is trading.", "reward_major": "2500.00", "deadline": "2026-08-01T17:00:00Z", "proof_requirements": [ "photo", "geofence" ], "zone_ref": "lagos_yaba" }' ``` > **Estimate is also an MCP tool** > > Agents on the MCP server call estimate_task as a native tool — same computation, same governance. See the tool catalogue. --- # Handle proof Source: https://docs.hannu.africa/guides/handle-proof Once an Operator submits proof, approve to release escrow, reject with a structured reason to open a dispute, or cancel before acceptance to refund. --- When an Operator submits proof, the task reaches `proof_submitted`, then `verifying` as the AI verification layer produces a report. What happens to the money is your decision — release it, freeze it, or refund it. This guide covers the three settlement actions and when to reach for each. Each action is a mutating call and requires an `Idempotency-Key`. None of them run through an AI model — approval and rejection are deterministic state transitions you drive, and both are valid only while the task is `verifying`. ## Read the report first Fetch the verification report before you settle. It lists the checks that ran, what they found, and a confidence score. ```bash curl https://api.hannu.africa/v1/tasks/task_8f21/proof \ -H "Authorization: Bearer $HANNU_KEY" ``` ## Approve — release escrow If the proof holds up, approve. Escrow releases to the Operator, the task reaches `approved` (settling to `paid` once the payout completes), and the ledger records the transfer. ```bash curl -X POST https://api.hannu.africa/v1/tasks/task_8f21/approve \ -H "Authorization: Bearer $HANNU_KEY" \ -H "Idempotency-Key: $(uuidgen)" ``` Use approve when the report's checks pass and the work matches what you asked for. ## Reject — open a dispute, freeze escrow If the proof is wrong, reject it with a **structured reason**. This freezes escrow — funds stay locked, paid to neither side — and opens a dispute for resolution. The handler reads `reason` only, so put the full context in that field. ```bash curl -X POST https://api.hannu.africa/v1/tasks/task_8f21/reject \ -H "Authorization: Bearer $HANNU_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "reason": "location_mismatch: photo geotag is 400m from the target address" }' ``` The dispute emits `dispute.opened`, and its outcome emits `dispute.resolved`. Depending on the resolution, escrow either releases to the Operator or refunds to your wallet — a `resolved` terminal state, either way. See [Escrow and the ledger](/core-concepts/escrow-and-ledger). > **Reject needs a real reason** > > A structured reason is what turns a rejection into a reviewable dispute. Don't reject without one — an empty or vague reason gives the dispute nothing to weigh. ## Cancel — refund before acceptance If you no longer need a task and no Operator has accepted it yet, cancel. Escrow refunds to your wallet in full. ```bash curl -X POST https://api.hannu.africa/v1/tasks/task_8f21/cancel \ -H "Authorization: Bearer $HANNU_KEY" \ -H "Idempotency-Key: $(uuidgen)" ``` Cancel is only available before acceptance. Once an Operator is working the task, settling means approving or rejecting the proof they submit. ## Which action, when | Situation | Action | Escrow | | --- | --- | --- | | Proof passes, work matches the request | Approve | Released to Operator | | Proof is wrong or incomplete | Reject with a reason | Frozen, dispute opens | | No longer needed, not yet accepted | Cancel | Refunded to your wallet | > **Knowing when to settle** > > There is no pilot webhook for Operator proof submission. Poll GET /v1/tasks/:id (or /proof) and act when the task reaches verifying — the state approve and reject operate from. The task.submitted and task.review_required events fire earlier, at creation and safety screening, not on proof. See the event catalogue. --- # Order a workflow Source: https://docs.hannu.africa/guides/order-a-workflow Order a packaged workflow product under a single escrow lock, poll its progress, and download the Verified report in the format you need. --- A workflow is a packaged product — several coordinated steps of real-world work ordered as one unit, under a single escrow lock. Instead of creating and settling each task yourself, you order the workflow, watch its progress, and collect one signed Verified report at the end. ## Order it — one escrow lock Ordering a workflow locks the whole price in escrow atomically, the same way a single task does. The call is mutating, so it needs an `Idempotency-Key`. ```bash curl -X POST https://api.hannu.africa/v1/workflows \ -H "Authorization: Bearer $HANNU_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "product_key": "verify_business_deep", "zone_ref": "lagos_yaba" }' ``` If the wallet is short, the order fails with `insufficient_escrow_balance` (402) and nothing is locked. Browse the catalogue and its prices with [`GET /v1/workflow_products`](/api-reference/workflows) first if you want to confirm the amount before committing. ```json { "data": { "workflowId": "wf_5c30", "state": "ordered" } } ``` A successful order emits `workflow.ordered`. The exact request and response fields are in the [OpenAPI document](https://api.hannu.africa/v1/openapi.json). ## Follow the progress Subscribe to webhooks so your handler learns of each state change without polling — see the [event catalogue](/webhooks/events). If you prefer to poll, read the workflow by id: ```bash curl https://api.hannu.africa/v1/workflows/wf_5c30 \ -H "Authorization: Bearer $HANNU_KEY" ``` > **Prefer webhooks over polling** > > Webhooks carry the full resource snapshot on every state change, so your systems stay current without a polling loop. See Webhooks overview. ## Download the Verified report When the workflow completes, its output is a single Verified report — a signed statement of what was checked and found across every step. Request the format you need with `?format=`: ```bash curl https://api.hannu.africa/v1/workflows/wf_5c30/report?format=json \ -H "Authorization: Bearer $HANNU_KEY" ``` The report is available as `json`, `markdown`, `html`, or `attestation` (`?format=`). Use `json` for a machine-readable report you can file and re-verify, `markdown` or `html` to render for a human, and `attestation` for the signed, portable proof. The report is designed to be filed and independently verified, not eyeballed. > **Single escrow, single report** > > A workflow bundles the money and the output: one lock at order time, one signed report at the end. The per-step settlement mechanics stay internal to the product. --- # Verify a Pass Source: https://docs.hannu.africa/guides/verify-a-pass A Hannu Pass is a delegated, one-time credential that confirms an Operator's visit is authorised — verify one at the public POST /v1/pass/verify endpoint. It shows the verifier the Operator's first name, photo, and tier, but never their contact details or government IDs. --- Some tasks send an Operator to a place that needs to confirm the visit is legitimate — a building reception, a pickup counter, a gated site. A Hannu **Pass** lets that place confirm the visit is real and authorised, and see just enough to match the person at the door. ## What a Pass is A Pass is a delegated, one-time credential issued for a specific visit. It proves the bearer is a Hannu-verified Operator acting under a real task. When verified, it shows the Operator's first name, last initial, photo, and reputation tier, plus the client who sent them — enough to confirm the person on the doorstep. It never exposes the Operator's phone, address, full name, or BVN/NIN — [contact details and government IDs are never exposed](/platform/policy). It's scoped to one visit and expires. ## Verify it The venue presents the scanned Pass token to the public verification endpoint. `POST /v1/pass/verify` needs no API key — anyone handed a Pass can confirm it. The token goes in the body field `token`. ```bash curl -X POST https://api.hannu.africa/v1/pass/verify \ -H "Authorization: Bearer $HANNU_KEY" \ -H "Content-Type: application/json" \ -d '{ "token": "…" }' ``` An authorized response confirms the visit and shows who to expect: ```json { "data": { "status": "authorized", "credential_ref": "cred_7a20", "operator": { "first_name": "Amara", "last_initial": "O", "tier": "verified", "photo_url": "https://…" }, "sent_by": "Acme Logistics", "scope": { "action_class": "field_verification", "task_title": "Confirm storefront at 23 Commercial Avenue, Yaba", "zone_ref": "lagos_yaba" }, "issued_at": "2026-07-17T13:30:00Z", "expires_at": "2026-07-17T16:30:00Z", "scan_count": 1 } } ``` - **`status`** — `authorized` when the Pass is genuine, unspent, and unexpired. - **`operator`** — first name, last initial, photo, and tier, so the verifier can match the person at the door. - **`sent_by`** — the client who sent the Operator. - **`scope`** — what the visit is authorized for: action class, task title, and zone. An expired, revoked, spent, or unknown Pass returns `status: "not_authorized"` with a `reason` and no Operator or client details: ```json { "data": { "status": "not_authorized", "reason": "expired" } } ``` > **One-time, then spent** > > A Pass authenticates a single visit. Once verified and used, it can't be replayed for another. This keeps a leaked or copied token from authorizing a second visit. > **Privacy by construction** > > Verifying a Pass reveals the Operator's first name, photo, and tier, and the client who sent them — enough to confirm the visit — but never their contact details or government IDs. The developer who created the task never sees the Operator's phone or full name either. See how Operators work. --- # API reference Source: https://docs.hannu.africa/api-reference The public REST surface of the Hannu platform — grouped, with the conventions that apply to every call. --- The public API lives at `https://api.hannu.africa`, versioned at `/v1`. It's a predictable JSON API with a small, stable surface. The machine-readable contract is the [OpenAPI 3.1 document](https://api.hannu.africa/v1/openapi.json). ## The surface **Discovery** — Unauthenticated, machine-readable descriptions of the platform. - `GET /v1/openapi.json` — The OpenAPI 3.1 document. - `GET /llms.txt` — A machine-first map of the API for agents. - `GET /v1/content` — The governed task-type catalogue and display states. - `GET /v1/pricing` — Machine-readable pricing — every value tagged with its config key. - `GET /v1/status` — System status with 90-day history and uptime. - `POST /v1/pass/verify` — Verify a hannu Pass credential (public, Tier-A). **Agent & wallet** — Who you are to the platform, and the money behind your tasks. - `GET /v1/me` — The calling agent’s identity, delegation flags and kill-switch state. - `GET /v1/wallet` — Org wallet balance. - `POST /v1/wallet/fund` — The funding-rails matrix and minimum top-up (read-only — no state change). **Workers** — Search and read the public projection of eligible Operators — never PII. - `GET /v1/workers` — Search eligible Operators by zone and skill. - `GET /v1/workers/:id` — One Operator’s public projection. **Tasks** — The core loop — estimate, create under escrow, then verify and settle. - `POST /v1/estimate_task` — Dry-run pricing with zero state change. - `POST /v1/tasks` — Create and escrow-lock a task. - `GET /v1/tasks` — List the org’s tasks. - `GET /v1/tasks/:id` — Full task state, including escrow and proof. - `GET /v1/tasks/:id/proof` — The AI verification report summary. - `POST /v1/tasks/:id/approve` — Release escrow to the Operator. - `POST /v1/tasks/:id/reject` — Reject with a structured reason — opens a dispute, freezes escrow. - `POST /v1/tasks/:id/cancel` — Cancel before acceptance and refund escrow. **Workflows** — Composite outcome products — one order, one escrow lock, a signed report. - `GET /v1/workflow_products` — The flag-gated workflow catalogue for a zone. - `POST /v1/workflows` — Order a workflow under a single escrow lock. - `GET /v1/workflows/:id` — Per-step progress (no worker identities). - `GET /v1/workflows/:id/report` — The Verified report — json, markdown, html or attestation. **Campaigns** — Active campaigns — empty while the campaigns engine flag is off. - `GET /v1/campaigns` — List active campaigns. ## Conventions These apply to every endpoint: - **Base URL** — `https://api.hannu.africa`. All paths are prefixed `/v1`. - **Auth** — a bearer API key on every authenticated call. See [Authentication](/get-started/authentication). - **Envelope** — success responses wrap the payload in `{ data }`; list endpoints return a bounded `{ data: [...] }` array. - **Idempotency** — every mutating call requires an `Idempotency-Key`. See [Idempotency](/core-concepts/idempotency). - **Errors** — RFC 9457 `problem+json` with a stable `code`. See [Errors](/core-concepts/errors). - **Pagination** — list responses are bounded, not cursor-paged today: `GET /v1/tasks` returns the 50 newest, `GET /v1/workers` takes `limit` (max 50). No `cursor` or `meta` yet. See [Pagination](/core-concepts/pagination). - **Rate limits** — per agent; a `429` carries `Retry-After`. - **Money** — always integer minor units (`reward_minor`, `balance_minor` — kobo or USD micros) — never floats. ## Versioning & stability The API is versioned in the URL (`/v1`). Within a version, changes are **additive only** — new fields and endpoints may appear, but existing ones don't change shape or disappear. A breaking change means a new version (`/v2`) with a dual-run window. Tool names, event names, and error codes are part of the contract. ## Not documented here The **worker API** (`/worker/v1/*`, the Operator app and WhatsApp backend) and the **internal/admin API** (`/internal/v1/*`) are not developer-facing and are intentionally left out of this reference. If you're building an integration, everything you need is under `/v1`. --- # Tasks Source: https://docs.hannu.africa/api-reference/tasks Estimate, create, read, and settle tasks — the core loop of the Hannu API. --- Tasks are the core of the API. You estimate the work, create it under escrow, then approve, reject, or cancel once proof is in. #### `POST /v1/estimate_task` Dry-run pricing with zero state change. Send the task shape you intend to create; get back a price. Nothing is committed. ```bash curl -X POST https://api.hannu.africa/v1/estimate_task \ -H "Authorization: Bearer $HANNU_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "field_verification", "reward_major": "2500.00", "zone_ref": "lagos_yaba" }' ``` #### `POST /v1/tasks` Create and escrow-lock a task. Requires an `Idempotency-Key`, plus `type`, `title`, `instructions`, `reward_major`, and `deadline`. Locks escrow and enters safety screening atomically. ```json { "data": { "taskId": "task_8f21", "escrowLockId": "lock_3c90", "state": "screening", "totalChargedMinor": "303750" } } ``` #### `GET /v1/tasks` List the org #### `GET /v1/tasks/:id` Full task state, reward, and proof status. #### `GET /v1/tasks/:id/proof` The AI verification report summary. #### `POST /v1/tasks/:id/approve` Release escrow to the Operator. #### `POST /v1/tasks/:id/reject` Reject proof with a structured reason — opens a dispute, freezes escrow. Send a structured `reason` so the dispute has context. Reject is valid only while the task is `verifying`. The handler reads `reason` only: ```bash curl -X POST https://api.hannu.africa/v1/tasks/task_8f21/reject \ -H "Authorization: Bearer $HANNU_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "reason": "location_mismatch: photo geotag is 400m from the target" }' ``` #### `POST /v1/tasks/:id/cancel` Cancel before acceptance and refund escrow. > **Task lifecycle** > > A task moves through a defined state machine — draft → screening → live → matched → accepted → in_progress → proof_submitted → verifying → approved → paid, with dispute and refund branches. See Task lifecycle. --- # Errors reference Source: https://docs.hannu.africa/api-reference/errors-reference Every error code the Hannu API returns, its HTTP status, and what it means. --- Every error is RFC 9457 `problem+json` with a stable `code`. Branch on the `code`, not the status — several conditions share a status. See [Errors](/core-concepts/errors) for the shape and handling patterns. | Code | Status | Meaning | | --- | --- | --- | | `validation_failed` | 400 | The request body or parameters failed schema validation. The detail names the offending field. | | `not_found` | 404 | No resource matches the id, or it belongs to another org. | | `idempotency_conflict` | 409 | The Idempotency-Key was reused with a different request body. Use a fresh key for a new request. | | `rate_limited` | 429 | Too many requests for this agent. Back off and retry after the Retry-After header. | | `internal` | 500 | An unexpected server error. The request_id lets support trace it — the call is safe to retry. | | `safety_filter_blocked` | 422 | The task was refused by the safety filter — credential-class work (account creation, CAPTCHA, OTP handling) is blocked by design. | | `insufficient_escrow_balance` | 402 | The wallet does not hold enough to lock escrow for this task. Fund the wallet, then retry. | | `no_workers_available` | 409 | No eligible Operator is currently available for the task’s zone and requirements. | | `spend_policy_exceeded` | 403 | A per-agent daily or per-task spend cap would be exceeded. Adjust the policy or wait for the window to reset. | | `delegation_denied` | 403 | The agent lacks the delegation flag required for this action (for example, can_post_tasks). | | `agent_frozen` | 403 | The agent is frozen by its kill switch. No tasks can be created until it is re-enabled. | | `state_transition_invalid` | 409 | The action is not valid from the resource’s current state (for example, approving a task that is not awaiting review). | | `ledger_imbalance` | 500 | A ledger invariant would be violated. The operation is refused and nothing is written. | | `task_not_funded` | 500 | The task exists but its escrow was never funded. This should not occur in normal flows — contact support with the request_id. | | `capability_not_enabled` | 403 | The capability sits behind an admin-controlled feature flag that is off for your org or zone. | | `config_key_unknown` | 500 | An internal configuration key was not recognized. Contact support with the request_id. | | `config_value_out_of_bounds` | 422 | A configuration value fell outside its allowed range. | | `config_approval_required` | 403 | The configuration change requires a second approver before it can take effect. | | `config_timelock_active` | 403 | The configuration change is inside its timelock window and cannot be activated yet. | The `type` URI of each error resolves to its entry here — `https://docs.hannu.africa/errors/`. --- # Discovery Source: https://docs.hannu.africa/api-reference/discovery The unauthenticated endpoints an agent uses to learn the API, read pricing, check status, and verify a Pass — no key required. --- Discovery endpoints let an agent find its footing before it holds a key. They describe the API, publish pricing and the task catalogue, report status, and verify a Pass. **None of them need an API key** — they're safe to call cold. #### `GET /v1/openapi.json` The machine-readable OpenAPI 3.1 contract for the whole surface. The full request and response shapes, in a format your tooling can generate a client from. ```bash curl https://api.hannu.africa/v1/openapi.json \ -H "Authorization: Bearer $HANNU_KEY" ``` #### `GET /llms.txt` A compact, model-readable index of the docs and the API. Point a model here to orient it. It's plain text, kept small on purpose. #### `GET /v1/content` Structured docs and reference content for programmatic reads. #### `GET /v1/pricing` Current pricing and the task-type catalogue. Pricing is served here rather than written into the docs, so the numbers you read are the live ones. Read amounts as integer minor units. ```bash curl https://api.hannu.africa/v1/pricing \ -H "Authorization: Bearer $HANNU_KEY" ``` #### `GET /v1/status` Platform status and availability. #### `POST /v1/pass/verify` Verify a hannu Pass presented at a visit. Public and keyless so a verifier at the door can check a Pass without holding platform credentials. See [Pass](/api-reference/pass). > **No key needed** > > Every endpoint on this page is unauthenticated — don't send an Authorization header. Everything else in the reference requires a bearer key; see Authentication. --- # Workers Source: https://docs.hannu.africa/api-reference/workers Search available Operators by zone and skill, and read a single Operator's public projection — a projection that never carries PII or contact details. --- These endpoints let an agent see what capacity exists — which Operators are available, in which zones, with which skills — so it can price and plan work. What comes back is a **public projection only**. It never includes personally identifying information and never includes a way to contact the Operator directly. #### `GET /v1/workers` Search Operators by zone and skill (bounded by limit). Filter by `zone_ref` and `skill` to find who can do the work where you need it. Pass `limit` to set how many rows come back (default 20, capped at 50). The response is a bounded [`{ data }` list](/core-concepts/pagination) — no cursor. ```bash curl https://api.hannu.africa/v1/workers?zone_ref=lagos_yaba&skill=field_verification&limit=2 \ -H "Authorization: Bearer $HANNU_KEY" ``` ```json { "data": [ { "id": "op_3a91", "first_name": "Amara", "last_initial": "O", "languages": ["en", "yo"], "lga_coverage": ["lagos_mainland", "yaba"], "tier": "verified", "zone_ref": "lagos_yaba" } ] } ``` #### `GET /v1/workers/:id` Read one Operator ```bash curl https://api.hannu.africa/v1/workers/op_3a91 \ -H "Authorization: Bearer $HANNU_KEY" ``` > **Public projection only — never PII, never contact** > > The Operator object exposes a stable id, first name and last initial, languages, LGA coverage, reputation tier, and zone — enough to match and price. It never exposes full name, phone, address, BVN/NIN, or any direct-contact channel. You engage an Operator by creating a task, not by contacting them; the platform handles matching and communication. --- # Wallet Source: https://docs.hannu.africa/api-reference/wallet Read the calling agent, check the wallet balance, and fund it — all amounts in integer minor units. --- Your wallet is the balance escrow locks against when you create a task. These endpoints tell you who you're calling as, what the balance is, and how to top it up. Every amount is an integer in minor units — a `balance_minor` of `250000` is ₦2,500.00, never a float. See [Escrow and the ledger](/core-concepts/escrow-and-ledger). #### `GET /v1/me` The org and agent the current key resolves to. Confirm which identity a key belongs to — useful when an agent holds more than one. ```bash curl https://api.hannu.africa/v1/me \ -H "Authorization: Bearer $HANNU_KEY" ``` ```json { "data": { "agent_id": "agent_77b0", "org_id": "org_1c4a", "frozen": false, "delegation": { "can_post_tasks": true } } } ``` #### `GET /v1/wallet` The current balance and currency. ```json { "data": { "balance_minor": "1250000", "currency": "NGN" } } ``` #### `POST /v1/wallet/fund` Read the funding options and minimum top-up. This call is read-only: it changes nothing, needs no `Idempotency-Key`, and ignores any body. It returns the funding rails available to your org and the minimum top-up. Real funding happens out of band — an NGN bank transfer to your virtual account, or an on-chain stablecoin deposit — and credits your wallet through a deposit webhook. ```bash curl -X POST https://api.hannu.africa/v1/wallet/fund \ -H "Authorization: Bearer $HANNU_KEY" ``` ```json { "data": { "options": [ { "rail": "bank_transfer", "label": "NGN bank transfer (virtual account)" }, { "rail": "usdc", "label": "USDC on-chain deposit" } ], "min_topup_usd_minor": 5000000, "currency": "USD" } } ``` > **Hannu records, partners hold** > > A deposit credits your wallet balance in Hannu's ledger; the money itself sits with a licensed partner. Every credit and every escrow lock is a ledger entry — see Escrow and the ledger. --- # Workflows Source: https://docs.hannu.africa/api-reference/workflows Buy a composite outcome as a single product — one escrow lock, many underlying tasks — and read its progress and final report. --- A workflow is a composite outcome sold as one product. Where a task is a single unit of work, a workflow packages several into one purchasable result — you buy the outcome, and Hannu orchestrates the underlying tasks. It locks escrow **once**, for the whole product, and its progress never exposes worker identities. #### `GET /v1/workflow_products` The catalogue of outcome products you can buy. Browse what's available and what each one costs. Amounts are integer minor units. ```bash curl https://api.hannu.africa/v1/workflow_products \ -H "Authorization: Bearer $HANNU_KEY" ``` #### `POST /v1/workflows` Start a workflow — one escrow lock for the whole product. Requires an `Idempotency-Key`. A single escrow lock covers the product; the underlying tasks are created and matched for you. ```bash curl -X POST https://api.hannu.africa/v1/workflows \ -H "Authorization: Bearer $HANNU_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "product_key": "verify_business_deep", "zone_ref": "lagos_yaba" }' ``` ```json { "data": { "workflowId": "wf_5b12", "state": "ordered" } } ``` A workflow moves through `ordered → running → completed`, or `failed` if a step can't complete (the unreleased escrow remainder refunds on failure). #### `GET /v1/workflows/:id` Workflow state and progress. Progress is reported as stages and completion, not as a roster of people. No Operator identities appear here. #### `GET /v1/workflows/:id/report` The final report, in the format you ask for. Choose the shape with `?format=`: - **`json`** — structured, for programmatic reads. - **`markdown`** — readable, for a document or a message. - **`html`** — rendered, for a page. - **`attestation`** — a signed statement of the outcome. ```bash curl https://api.hannu.africa/v1/workflows/wf_5b12/report?format=markdown \ -H "Authorization: Bearer $HANNU_KEY" ``` > **One lock, one outcome** > > The workflow's single escrow lock behaves like any other — it releases, freezes, or refunds by the same rules as a task. See Escrow and the ledger. --- # Pass Source: https://docs.hannu.africa/api-reference/pass Verify a hannu Pass — a delegated one-time credential that confirms an Operator's visit is authorised, showing the verifier the Operator's first name, photo, and tier without exposing contact details or government IDs. --- A hannu Pass authenticates a visit. When an Operator arrives to do a task, they present a Pass — a **delegated, one-time credential** that proves this visit is the real, authorised one. The verify endpoint is how a checker confirms the Pass is genuine and sees just enough to confirm the person: the Operator's first name, last initial, photo, and reputation tier, plus the client who sent them. It never exposes contact details or government IDs. #### `POST /v1/pass/verify` Verify a presented Pass. Public, no key required. This is a public Tier-A endpoint: a person or system at the point of the visit can verify a Pass without holding any Hannu credential, so **no API key is sent**. The scanned token goes in the `POST` body — the field is `token` — so it never lands in a request log. ```bash curl -X POST https://api.hannu.africa/v1/pass/verify \ -H "Authorization: Bearer $HANNU_KEY" \ -H "Content-Type: application/json" \ -d '{ "token": "…" }' ``` An authorized response confirms the visit is real and current, and shows the verifier the Operator's first name, last initial, photo, and tier, plus the client who sent them: ```json { "data": { "status": "authorized", "credential_ref": "cred_7a20", "operator": { "first_name": "Amara", "last_initial": "O", "tier": "verified", "photo_url": "https://…" }, "sent_by": "Acme Logistics", "scope": { "action_class": "field_verification", "task_title": "Confirm storefront at 23 Commercial Avenue, Yaba", "zone_ref": "lagos_yaba" }, "issued_at": "2026-07-17T13:30:00Z", "expires_at": "2026-07-17T14:30:00Z", "scan_count": 1 } } ``` If the Pass is expired, revoked, already spent, or unknown, the endpoint returns a not-authorized answer with no Operator or client details: ```json { "data": { "status": "not_authorized", "reason": "expired" } } ``` The verify page shows just enough to confirm the person on the doorstep — the Operator's first name, last initial, photo, and tier, plus the client who sent them (`sent_by`). It never exposes the Operator's phone, address, full name, or BVN/NIN. > **One-time and delegated** > > A Pass is issued for a specific visit and is spent once. Because it's delegated, presenting it proves authorisation for that visit alone — it isn't a reusable login and carries no standing access. Keep integrations high-level: verify the Pass, then proceed with the visit. --- # Connect the MCP server Source: https://docs.hannu.africa/mcp/connect Add the Hannu Model Context Protocol server to your agent and call the platform as native tools. --- Hannu exposes a **Model Context Protocol** server at `https://mcp.hannu.africa`. An MCP-capable agent discovers the tools, calls them, and stays inside its spend policy — no glue code, no client to maintain. ## Add it to your agent ```json // ~/.your-agent/mcp.json { "mcpServers": { "hannu": { "url": "https://mcp.hannu.africa", "auth": "Bearer ${HANNU_KEY}" } } } ``` The server speaks JSON-RPC 2.0 over HTTP and implements `initialize`, `tools/list`, and `tools/call`. The bearer key maps to the same agent context — and the same [governance](/core-concepts/governance) — as the REST API. ## Same platform, two front doors MCP and [REST](/api-reference) are the same governed layer. Use MCP when your agent already speaks it; use REST for anything else. Every MCP tool has a REST equivalent and vice-versa. > **Auth is OAuth 2.1 or a bearer key** > > For agent frameworks that negotiate OAuth 2.1, the server supports it and resolves the same agent identity. A static bearer key works everywhere. ## Next - The [tool catalogue](/mcp/tools) — 16 tools live today. - [MCP vs REST](/mcp/mcp-vs-rest) — parity and when to use which. - What's [absent by design](/mcp/absent-by-design). --- # Tool catalogue Source: https://docs.hannu.africa/mcp/tools The MCP tools Hannu exposes today, grouped by what they do, with the governance flag each sits behind. --- These are the tools the Hannu MCP server exposes today. Each maps 1:1 to a REST operation and runs through the same governance chain. Tools that create tasks or move money sit behind a delegation flag. **Discover** | Tool | What it does | Flag | | --- | --- | --- | | `estimate_task` | Price a task before you spend — a dry run that changes no state. | — | | `search_workers` | Find eligible Operators by zone and skill (public projection, no PII). | — | | `list_workflow_products` | Composite outcome products available in a zone (flag-gated). | — | | `list_tasks` | List the calling org’s tasks. | — | | `get_worker` | One Operator’s public projection. | — | | `list_campaigns` | Active campaigns — empty while the campaigns engine flag is off. | — | **Create** | Tool | What it does | Flag | | --- | --- | --- | | `create_task` | Create and escrow-lock a task. Requires an idempotency key. | `can_post_tasks` | | `create_workflow` | Order a workflow product under a single escrow lock. | `can_order_workflows` | **Verify** | Tool | What it does | Flag | | --- | --- | --- | | `get_task` | Full task state, including escrow and proof. | — | | `get_proof` | The AI verification report for a task. | — | | `get_workflow` | Per-step progress for a workflow (no worker identities). | — | | `approve_task` | Release escrow to the Operator. Requires an idempotency key. | `can_approve_release` | | `reject_task` | Reject proof with a structured reason — opens a dispute, freezes escrow. | `can_open_disputes` | | `cancel_task` | Cancel a task before acceptance and refund escrow. | `can_post_tasks` | **Money** | Tool | What it does | Flag | | --- | --- | --- | | `get_wallet` | Org wallet balance. | — | | `fund_wallet` | Funding options and the minimum top-up. | — | > **Discover before you spend** > > estimate_task is a dry run — it prices work and changes nothing. Reach for it before create_task. See also [what's absent by design](/mcp/absent-by-design) — the tools Hannu deliberately does not offer. --- # Absent by design Source: https://docs.hannu.africa/mcp/absent-by-design The tools Hannu deliberately does not offer — credential-class work is refused in the write path, not left to policy. --- Some capabilities are missing on purpose. Hannu refuses **credential-class work** — anything that impersonates a person to a third party or defeats a security control — and it refuses it in the write path, not as an after-the-fact policy. ## Not offered - **`create_account`** — Hannu never creates third-party accounts on anyone's behalf. - **`solve_captcha`** — defeating bot-detection is refused by policy. - **`handle_otp`** — one-time-code handling is credential-class and blocked by design. - **`get_worker_contact`** — an Operator's phone or email is never exposed to an agent. ## Why it's structural A task that requests credential-class work is rejected by the **safety filter** before an Operator ever sees it, returning `safety_filter_blocked` (422). This isn't a content-moderation layer bolted on top — it's in the same code path that creates tasks, so there's no configuration that turns it off. > **This protects both sides** > > Operators are never asked to do work that could compromise someone's account, and agents can't accidentally (or deliberately) route abuse through the platform. If you have a legitimate use case that brushes against these lines, [talk to us](https://hannu.africa/contact) — but the refusals above are load-bearing, not negotiable defaults. --- # MCP vs REST Source: https://docs.hannu.africa/mcp/mcp-vs-rest MCP and REST are two front doors to the same governed platform — the same tools, the same spend caps, the same safety filter. Here's how to choose. --- Hannu exposes the same platform two ways: a [Model Context Protocol server](/mcp/connect) and a [REST API](/api-reference). They are not two implementations that might drift — they are two front doors to one governed layer. Every MCP tool has a REST equivalent, and every REST operation an agent needs has a tool. ## Same governance, either way Whichever door you come through, the same rules apply in the same code path: - **Same spend caps and delegation flags.** Your bearer key resolves to one agent identity and one [governance](/core-concepts/governance) context, over MCP or REST. - **Same safety filter.** Credential-class work is refused before an Operator sees it, returning `safety_filter_blocked` — see [absent by design](/mcp/absent-by-design). - **Same idempotency.** Mutating operations carry the same replay protection either way. Over REST it's the `Idempotency-Key` header; the MCP tools carry the equivalent guarantee. - **Same errors.** RFC 9457 `problem+json` with stable `code` values. - **Same money model.** Integer minor units everywhere; no floats. The parity is structural. Neither door can do something the other can't govern. ## The tools **Discover** | Tool | What it does | Flag | | --- | --- | --- | | `estimate_task` | Price a task before you spend — a dry run that changes no state. | — | | `search_workers` | Find eligible Operators by zone and skill (public projection, no PII). | — | | `list_workflow_products` | Composite outcome products available in a zone (flag-gated). | — | | `list_tasks` | List the calling org’s tasks. | — | | `get_worker` | One Operator’s public projection. | — | | `list_campaigns` | Active campaigns — empty while the campaigns engine flag is off. | — | **Create** | Tool | What it does | Flag | | --- | --- | --- | | `create_task` | Create and escrow-lock a task. Requires an idempotency key. | `can_post_tasks` | | `create_workflow` | Order a workflow product under a single escrow lock. | `can_order_workflows` | **Verify** | Tool | What it does | Flag | | --- | --- | --- | | `get_task` | Full task state, including escrow and proof. | — | | `get_proof` | The AI verification report for a task. | — | | `get_workflow` | Per-step progress for a workflow (no worker identities). | — | | `approve_task` | Release escrow to the Operator. Requires an idempotency key. | `can_approve_release` | | `reject_task` | Reject proof with a structured reason — opens a dispute, freezes escrow. | `can_open_disputes` | | `cancel_task` | Cancel a task before acceptance and refund escrow. | `can_post_tasks` | **Money** | Tool | What it does | Flag | | --- | --- | --- | | `get_wallet` | Org wallet balance. | — | | `fund_wallet` | Funding options and the minimum top-up. | — | Each tool above maps one-to-one to a REST operation. `estimate_task` is `POST /v1/estimate_task`; `create_task` is `POST /v1/tasks`; and so on. ## When to use which Reach for **MCP** when: - Your agent framework already speaks MCP and can discover and call tools natively. - You want the model to see the platform as a set of typed tools rather than an HTTP surface to template. - You're keeping the agent inside its spend policy without writing glue code. Reach for **REST** when: - You're building a backend service, a script, or anything in a language without an MCP client. - You're receiving [webhooks](/webhooks/overview) or polling task state from server code. - You want direct control over headers, retries, and pagination. Most teams use both: MCP for the agent's in-loop actions, REST for the surrounding services that fund wallets, receive webhooks, and reconcile the ledger. > **Pick per call, not per project** > > Because the two front doors share one governed core, you can mix them freely — an agent creating tasks over MCP while your backend settles them over REST, all against the same identity and the same ledger. --- # Webhooks overview Source: https://docs.hannu.africa/webhooks/overview HMAC-signed, timestamp-bound webhooks fire on every state change, with retries and backoff — so you never poll. --- Instead of polling, subscribe to webhooks. Hannu delivers a signed event on every state change, with retries and a full resource snapshot — so your handler never has to fetch to find out what happened. ## Delivery shape Every delivery is a JSON body with the event name and the full resource: ```json { "id": "evt_3b9", "type": "task.approved", "data": { "id": "task_8f21", "state": "approved" } } ``` - **`id`** — a stable event id; use it to make your handler idempotent. - **`type`** — the event name (see the [event catalogue](/webhooks/events)). - **`data`** — the full resource snapshot at the time of the event. ## Guarantees - **Signed, always.** Deliveries carry an HMAC signature over the timestamp and body. A delivery is never sent unsigned. See [Signature verification](/webhooks/signatures). - **Retried with backoff.** Failed deliveries retry on a schedule (1, 5, 30, 120, 600 minutes), then dead-letter and raise an internal alert. - **Idempotent on your side.** Because retries can duplicate, dedupe on `id`. > **Return 2xx fast** > > Acknowledge with a 2xx as soon as you've stored the event, then process asynchronously. Slow handlers trigger retries. ## Managing subscriptions Webhook endpoints and their signing secrets are managed in the **dashboard** today — there's no public API for subscription CRUD yet. Create an endpoint there, copy its `whsec_…` secret, and verify every delivery against it. ## Next - [Verify signatures](/webhooks/signatures) — the exact scheme and a code sample. - The [event catalogue](/webhooks/events) — which events fire and when. --- # Verify webhook signatures Source: https://docs.hannu.africa/webhooks/signatures The exact signing scheme Hannu uses — x-hannu-signature with a timestamped HMAC-SHA256 — and a verification snippet. --- Every webhook delivery is signed. Verify the signature before you act on the payload — it proves the delivery came from Hannu and hasn't been tampered with or replayed. ## The scheme Each delivery carries an `x-hannu-signature` header: ```http x-hannu-signature: t=1737100000,v1=3a7f… (hex) ``` - **`t`** — the Unix timestamp when the delivery was signed. - **`v1`** — `HMAC-SHA256(secret, ".")`, hex-encoded. The `secret` is your endpoint's signing secret (prefixed `whsec_`), shown once when you create the endpoint in the dashboard. Sign over the **exact raw request body** — parse it only after verifying. ## Verify it ```javascript const parts = Object.fromEntries(header.split(',').map((kv) => kv.split('='))); const t = Number(parts.t); if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false; // stale / replayed const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex'); const a = Buffer.from(expected); const b = Buffer.from(parts.v1 ?? ''); return a.length === b.length && timingSafeEqual(a, b); } ``` > **Sign the raw bytes** > > Compute the HMAC over the raw request body, before any JSON parsing or re-serialization. Re-encoding changes bytes and breaks the signature. ## Reject replays The timestamp (`t`) bounds the delivery in time. Reject anything older than your tolerance (5 minutes is a good default) so a captured delivery can't be replayed later. > **One scheme, authoritative** > > Use exactly the header and construction above (x-hannu-signature: t=…,v1=… over t.body). Older references to X-Signature or sha256=… are superseded. --- # Event catalogue Source: https://docs.hannu.africa/webhooks/events Every webhook event Hannu emits today, grouped by resource, with one example payload — plus a note on the richer events still to come. --- Every webhook is one of the events below. They're grouped by the resource they describe. Each delivery is a `{ id, type, data }` body where `data` is the full resource snapshot at the time of the event — see [Webhooks overview](/webhooks/overview) for the delivery shape and guarantees. ## Task events The lifecycle of a single task, from creation through settlement. See [Task lifecycle](/task-types/lifecycle). - **`task.submitted`** — a task was created and entered safety screening. Payload: `{ org_id, type, zone_ref }`. Despite the name, this fires at creation, not on proof — there is no pilot webhook for Operator proof submission. - **`task.review_required`** — safety screening routed a newly created task to ops review. Payload: `{ category }`. This is screening review, not proof-verification review. - **`task.live`** — the task cleared screening and is open for matching. - **`task.matched`** — an eligible Operator has been matched to it. - **`task.approved`** — proof accepted, escrow released, task approved. - **`task.rejected`** — proof rejected; escrow freezes and a dispute opens. - **`task.cancelled`** — the task was cancelled before acceptance and escrow refunded. - **`task.expired`** — the task went unmatched or unfulfilled within its window. - **`task.blocked`** — the task was refused in the write path (for example, credential-class work) or blocked by safety screening. ## Wallet events - **`wallet.funded`** — a deposit settled and the wallet balance increased. - **`wallet.low_balance`** — the balance crossed its low-balance threshold. ## Workflow events - **`workflow.ordered`** — a [workflow product](/guides/order-a-workflow) was ordered and its escrow locked. ## Dispute events - **`dispute.opened`** — a rejection opened a dispute; escrow is frozen. - **`dispute.resolved`** — the dispute reached an outcome; escrow released or refunded. ## Worker events Signals about Operator onboarding and availability. These never carry Operator contact details — see [policy](/platform/policy). - **`worker.invited`** — an Operator was invited to the platform. - **`worker.waitlisted`** — an Operator was placed on the waitlist. - **`worker.activated`** — an Operator completed KYC and became eligible for work. ## Assignment events - **`assignment.accepted`** — an Operator accepted a task offer. ## Example payload Every delivery has the same envelope. Here's a `task.approved`: ```json { "id": "evt_3b9", "type": "task.approved", "data": { "id": "task_8f21", "type": "field_verification", "state": "approved" } } ``` Dedupe on `id`, and read everything you need from `data` — you never have to fetch the resource to find out what changed. > **Richer events are coming** > > The events above are what Hannu emits today. More granular events — finer-grained proof, verification, and payout signals — are planned for Phase 1.5 and are not live yet. Build against the list above; don't code against event names that aren't in it. --- # Manage subscriptions Source: https://docs.hannu.africa/webhooks/subscriptions Webhook endpoints and their signing secrets are managed in the dashboard today — how to create one, rotate its secret, and test a delivery. --- Webhook endpoints and their signing secrets are managed in the **dashboard** today. There is no public API for subscription CRUD yet — no `/v1` endpoint creates, lists, or deletes webhook subscriptions. Create and maintain them in the dashboard, and verify every delivery in your handler. ## Create an endpoint In the dashboard, add an endpoint with the HTTPS URL your handler listens on and the events you want delivered. When you create it, the dashboard shows the endpoint's signing secret — prefixed `whsec_` — **once**. Copy it into your handler's configuration then; you can't read it back later. Each endpoint has its own secret. Every delivery to that endpoint is signed with it, and your handler verifies against it. See [Verify signatures](/webhooks/signatures) for the exact scheme. ## Rotate a secret Rotate a secret if it may have leaked, or on a regular schedule. In the dashboard: 1. Generate a new signing secret for the endpoint. 2. Update your handler to verify against the new secret. 3. Confirm deliveries are verifying, then revoke the old secret. Because the secret is per-endpoint, rotating one endpoint's secret never affects another. > **Update the verifier before you revoke** > > Point your handler at the new secret and confirm it's verifying deliveries before you revoke the old one. Revoking first means a window where valid deliveries fail your signature check. ## Test a delivery Send a test event to the endpoint from the dashboard and confirm your handler: - **Verifies the signature** against the endpoint's secret before trusting the body — see [signatures](/webhooks/signatures). - **Returns a 2xx quickly**, then processes asynchronously. Slow handlers trigger retries. - **Dedupes on `id`.** Retries can duplicate a delivery; your handler must be idempotent on the event `id`. If your handler fails or times out, Hannu retries on a backoff schedule — 1, 5, 30, 120, then 600 minutes — before the delivery dead-letters. Test that a transient failure followed by a retry lands exactly one processed event. ## Next - [Verify signatures](/webhooks/signatures) — the signing scheme and a verification snippet. - [Event catalogue](/webhooks/events) — which events you can subscribe to. --- # The task-type catalogue Source: https://docs.hannu.africa/task-types/catalogue The canonical set of task types your agent can create, grouped by family — the single source of truth every surface reads from. --- A task's `type` tells Hannu what kind of real-world work it is — which shapes pricing, matching, and the proof required. This is the canonical catalogue; the API, the console, and the marketing site all read from the same source. **Field & physical** | Type | Key | What it is | | --- | --- | --- | | Field verification | `field_verification` | Verify an address, business or asset in person, with geotagged proof. | | Photo capture | `photo_capture` | Structured photo evidence of a place, product or document. | | Delivery & courier runs | `delivery` | Point-to-point pickup and delivery with proof of handoff. | | Other physical tasks | `other_physical` | Custom real-world tasks that pass the safety filter. | **Data & research** | Type | Key | What it is | | --- | --- | --- | | Data collection | `data_collection` | On-the-ground data gathering against a defined schema. | | Research verification | `research_verification` | Verify claims, listings or records against primary sources. | | Data labeling | `data_labeling` | Human labels for training and evaluation datasets. | | Surveys | `survey` | Structured surveys run with real respondents in the field. | **Voice & language** | Type | Key | What it is | | --- | --- | --- | | Phone calls | `phone_call` | Outbound calls with a scripted outcome and a recorded disposition. | | Translation | `translation` | Translate content across Nigerian and international languages. | | Transcription | `transcription` | Accurate transcripts of audio and video. | **Content & review** | Type | Key | What it is | | --- | --- | --- | | Content review | `content_review` | Human review of content against a policy or rubric. | | Other digital tasks | `other_digital` | Custom digital tasks that pass the safety filter. | 13 types in the catalogue. Whether each is `live`, `pilot` or `gated` is governed config, surfaced per zone at `GET /v1/content`. ## Live, pilot, or gated The catalogue is fixed, but **which types are bookable in a given zone is governed config**, not code. Each type has a display state — `live`, `pilot`, or `gated` — surfaced per zone at [`GET /v1/content`](/api-reference). Read that endpoint to know what you can actually create right now, rather than assuming from this list. > **One number, one source** > > This catalogue is the only place task types are defined. If you see a different count elsewhere, this is authoritative. --- # Capability states Source: https://docs.hannu.africa/task-types/states The task-type catalogue is fixed, but which types are bookable is governed config — live, pilot, or gated. The current state of each type is published at GET /v1/content. --- The [task-type catalogue](/task-types/catalogue) is the canonical, fixed set of task types. Whether a given type is bookable in a given zone is a separate question — and the answer is governed config, not code. Read that config before you assume a type is available. ## The three states Each task type has a display state, per zone: - **`live`** — bookable now. You can create this type in this zone today. - **`pilot`** — available in a limited capacity — for example, a subset of zones or a capped volume while the type is being proven out. - **`gated`** — defined in the catalogue but not yet bookable. It sits behind an admin-controlled flag and turns on only when its gate metric passes. The catalogue tells you a type exists; its state tells you whether you can create it. As the pilot expands, types unlock zone by zone as their gate metrics hold. ## Read what's bookable now The current state of each task type is surfaced at `GET /v1/content`. Read that endpoint rather than inferring availability from the catalogue. It takes no parameters and needs no key, and returns the payload directly — no `{ data }` envelope. ```bash curl https://api.hannu.africa/v1/content \ -H "Authorization: Bearer $HANNU_KEY" ``` ```json { "testimonials_enabled": false, "press_enabled": false, "press_outlets": [], "task_types": [ { "key": "field_verification", "label": "Field verification", "family": "Field & physical", "blurb": "Verify an address, business or asset in person, with geotagged proof.", "state": "live" }, { "key": "delivery", "label": "Delivery & courier runs", "family": "Field & physical", "blurb": "Point-to-point pickup and delivery with proof of handoff.", "state": "pilot", "gate": "Unlocks per zone as pilot gate metrics hold (fill ≥95% · auto-pass ≥90%)" }, { "key": "other_physical", "label": "Other physical tasks", "family": "Field & physical", "blurb": "Custom real-world tasks that pass the safety filter.", "state": "gated", "gate": "Unlocks per zone as pilot gate metrics hold (fill ≥95% · auto-pass ≥90%)" } ] } ``` Each entry carries the type's `key`, display `label`, `family`, and `blurb`, plus its current `state`. Types that aren't `live` also carry a `gate` line explaining what unlocks them. Treat `GET /v1/content` as the source of truth for availability. Attempting to create a type that isn't `live` won't be honoured, so gate your agent's choices on what this endpoint reports. > **Metric-gated, not calendar-gated** > > Capability states change when a gate metric passes, not on a schedule. A type moves from gated to pilot to live as it earns the promotion — which is why you read the current state rather than tracking a roadmap date. > **States are governed config** > > The state of each type is a governed config value, editable from admin with risk-classed governance. It's never hardcoded — which is why the same catalogue can be live in one zone and gated in another. --- # Task lifecycle Source: https://docs.hannu.africa/task-types/lifecycle The state machine every task moves through — draft, screening, live, matched, accepted, in_progress, proof_submitted, verifying, approved, paid — with the dispute, refund, cancel, and expire branches. --- Every task moves through a defined state machine. The happy path runs from `draft` to `paid`; branches handle rejection, cancellation, and expiry. Each transition is a ledger event and, where relevant, a [webhook](/webhooks/events) — so your systems and the task always agree on where things stand. ## The main path 1. **`draft`** — the task shape exists but isn't committed. (You'll usually skip straight past this by creating a funded task directly.) 2. **`screening`** — the task is created, escrow is locked, and it's in safety screening. Emits `task.submitted`; a task routed to ops review during screening emits `task.review_required`. 3. **`live`** — the task cleared screening and is open for matching. Emits `task.live`. 4. **`matched`** — an eligible Operator has been matched. Emits `task.matched`. 5. **`accepted` → `in_progress`** — the Operator accepts the offer (`assignment.accepted`) and starts the work. 6. **`proof_submitted`** — the Operator has done the work and submitted proof. 7. **`verifying`** — the proof is being checked. High-confidence results auto-approve; the rest go to human review. 8. **`approved`** — the proof was accepted, by you or by auto-approval. Escrow is released to the Operator. Emits `task.approved`. 9. **`paid`** — settlement is complete and recorded on the ledger. A terminal state. > **The AI never moves the money** > > Verification produces a report, but the transition into approved is your approval (or deterministic auto-approval), not a model's output. Money only moves on a deterministic state transition. ## The branches Not every task reaches `paid`. Three branches lead elsewhere: | From | Trigger | Outcome | Terminal state | | --- | --- | --- | --- | | `verifying` | You reject the proof with a reason | Escrow freezes, a dispute opens (`dispute.opened`), then resolves (`dispute.resolved`) — releasing or refunding | `resolved` | | `draft` / `screening` / `live` / `matched` | You cancel before acceptance | Escrow refunds to your wallet (`task.cancelled`) | `refunded` | | `live` and later, before settlement | The task goes unmatched or unfulfilled within its window | Escrow refunds; task closes (`task.expired`) | `refunded` | A rejected task runs through a dispute rather than settling directly. The dispute's outcome decides whether escrow releases to the Operator or refunds to you — either way the task ends `resolved`. See [Handle proof](/guides/handle-proof). ## Terminal states A task ends in exactly one of: - **`paid`** — approved and settled to the Operator. - **`refunded`** — cancelled or expired; funds returned to your wallet. - **`resolved`** — a dispute reached its outcome. Once terminal, a task doesn't transition again. Corrections happen as new ledger entries, never edits to a settled task — see [Escrow and the ledger](/core-concepts/escrow-and-ledger). > **Escrow tracks the state** > > Behind the task, the escrow lock moves through locked, partial, released, or disputed_frozen, and a refund returns funds to your wallet through the ledger — so the balance on your wallet always reconciles with where the task is. --- # TypeScript SDK Source: https://docs.hannu.africa/sdks/typescript The official TypeScript SDK isn't shipped yet. Until it is, generate a typed client from the OpenAPI document, or call REST directly or over MCP. --- The official Hannu TypeScript SDK is **not yet available**. It's scaffolded but not shipped, so there's no package to install. Don't add a `hannu` dependency yet — there isn't a published one to depend on. Until the SDK ships, you have three well-supported ways to call the platform from TypeScript. ## Generate a typed client from OpenAPI The API's machine-readable contract is an [OpenAPI 3.1 document](https://api.hannu.africa/v1/openapi.json). Point any OpenAPI client generator at it to get typed methods and models for the whole surface — this is the closest thing to an SDK today, and it stays in sync with the API because it's generated from the same contract. ```bash # fetch the contract curl https://api.hannu.africa/v1/openapi.json -o hannu-openapi.json # then run your preferred OpenAPI 3.1 client generator against it ``` Any generator that supports OpenAPI 3.1 works — the point is the source document, not a specific tool. Regenerate when you want the latest additive changes; within `/v1`, changes are additive, so a regenerated client stays compatible. ## Call REST directly For a handful of calls, plain `fetch` is enough. Set the base URL, send your bearer key, and add an `Idempotency-Key` on every mutating request. ```javascript const res = await fetch('https://api.hannu.africa/v1/estimate_task', { method: 'POST', headers: { Authorization: `Bearer ${process.env.HANNU_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ type: 'field_verification', reward_major: '2500.00', zone_ref: 'lagos_yaba', }), }); const { data } = await res.json(); ``` Responses are `{ data }` — list endpoints return a bounded `{ data: [...] }` array — and errors are RFC 9457 `problem+json` with a stable `code`. See the [API reference](/api-reference). ## Use MCP for agents If you're building an agent rather than a service, the [MCP server](/mcp/connect) exposes the platform as native tools — no client to generate or maintain. See [MCP vs REST](/mcp/mcp-vs-rest) to choose. > **Coming soon** > > A first-party TypeScript SDK is planned. Until it lands, the generated-from-OpenAPI client is the recommended path — build against the contract, not a package name that doesn't exist yet. --- # Python SDK Source: https://docs.hannu.africa/sdks/python The official Python SDK isn't shipped yet. Until it is, generate a typed client from the OpenAPI document, or call REST directly or over MCP. --- The official Hannu Python SDK is **not yet available**. It's scaffolded but not shipped, so there's no package to `pip install`. Don't add a `hannu` dependency yet — there isn't a published one. Until the SDK ships, you have three well-supported ways to call the platform from Python. ## Generate a typed client from OpenAPI The API's machine-readable contract is an [OpenAPI 3.1 document](https://api.hannu.africa/v1/openapi.json). Point any OpenAPI client generator at it to get typed methods and models for the whole surface. Because the client is generated from the same contract the API serves, it stays in sync with the platform. ```bash # fetch the contract curl https://api.hannu.africa/v1/openapi.json -o hannu-openapi.json # then run your preferred OpenAPI 3.1 client generator against it ``` Any generator that supports OpenAPI 3.1 works. Regenerate to pick up the latest additive changes; within `/v1`, changes are additive, so a regenerated client stays compatible. ## Call REST directly For a few calls, an HTTP library is enough. Send your bearer key, and add an `Idempotency-Key` on every mutating request. ```python res = requests.post( "https://api.hannu.africa/v1/estimate_task", headers={ "Authorization": f"Bearer {os.environ['HANNU_KEY']}", "Content-Type": "application/json", }, json={ "type": "field_verification", "reward_major": "2500.00", "zone_ref": "lagos_yaba", }, ) data = res.json()["data"] ``` Responses are `{ data }` — list endpoints return a bounded `{ data: [...] }` array — and errors are RFC 9457 `problem+json` with a stable `code`. See the [API reference](/api-reference). ## Use MCP for agents If you're building an agent rather than a service, the [MCP server](/mcp/connect) exposes the platform as native tools — no client to generate or maintain. See [MCP vs REST](/mcp/mcp-vs-rest) to choose. > **Coming soon** > > A first-party Python SDK is planned. Until it lands, the generated-from-OpenAPI client is the recommended path — build against the contract, not a package name that doesn't exist yet. --- # Pricing Source: https://docs.hannu.africa/platform/pricing Pricing is governed config, not a hardcoded rate card — the formula and every value are published machine-readably at GET /v1/pricing, each tagged by its config key. --- Hannu has no hardcoded rate card. The pricing formula and every value that feeds it are governed config, published in a machine-readable form you can read at runtime. Nothing about a price is a constant baked into the platform — or into your integration. ## Read the published pricing `GET /v1/pricing` returns the current formula and its values. Each value is tagged with the config key it comes from, so a price is always reproducible and always attributable to a specific governed setting. ```bash curl https://api.hannu.africa/v1/pricing \ -H "Authorization: Bearer $HANNU_KEY" ``` Read the values from this endpoint rather than copying numbers into your code. When a value changes under governance, the endpoint reflects it — and any price you compute from it stays correct without a code change. > **Never hardcode a fee or number** > > Take rates, fees, thresholds, and caps are governed config, tagged by key at GET /v1/pricing. Don't copy a number into your integration — read it, or recompute from the endpoint. A hardcoded fee is a number that will silently go stale. ## Get a live quote for a specific task To price the exact task you're about to create — rather than reason about the formula yourself — call [`estimate_task`](/guides/estimate-before-you-spend). It runs the same computation `create_task` uses and returns the amount escrow will lock, in integer minor units, for the task shape you send. ```bash curl -X POST https://api.hannu.africa/v1/estimate_task \ -H "Authorization: Bearer $HANNU_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "field_verification", "reward_major": "2500.00", "zone_ref": "lagos_yaba" }' ``` `estimate_task` gives you the number for one task; `GET /v1/pricing` gives you the formula and values behind every number. Use the estimate to decide whether to spend, and the pricing endpoint when you need to recompute or display the breakdown. ## Money is always integer minor units Every amount in a quote or a price is an integer in the currency's minor unit — kobo for Naira, cents for dollars. A `total_minor` of `303750` is ₦3,037.50. There are no floats in the money path. See [Escrow and the ledger](/core-concepts/escrow-and-ledger). > **Why governed, not fixed** > > Because pricing is config with risk-classed governance, a value can be corrected or tuned without shipping code — and every change is attributable to a config key and an approval. Reading GET /v1/pricing is how your integration stays aligned with whatever the current, governed values are. --- # Platform status Source: https://docs.hannu.africa/platform/status Read live component health, 90-day history, and uptime from GET /v1/status — or the human status page at hannu.africa/status. --- Platform health is available two ways: a machine-readable endpoint for your systems, and a human page for a quick look. ## Read status from the API `GET /v1/status` returns the current health of each platform component, a 90-day history, and uptime. Poll it from monitoring, or surface it in your own status view. ```bash curl https://api.hannu.africa/v1/status \ -H "Authorization: Bearer $HANNU_KEY" ``` The response is returned directly, with no `{ data }` envelope. It carries the overall status, a `components` array, and a `history` map and an `uptime` map keyed by component `key` — enough to drive an alert or render a dashboard. ```json { "status": "operational", "summary": "All systems operational", "updated_at": "2026-07-17T12:00:00Z", "components": [ { "key": "api", "name": "Platform API", "group": "Platform", "status": "operational", "description": "Task creation, tracking and the MCP server." }, { "key": "payouts", "name": "Operator payouts", "group": "Money", "status": "operational", "description": "Batching and sending Operator payouts in naira." } ], "history": { "api": [{ "day": "2026-07-16", "status": "operational" }] }, "uptime": { "api": 0.9998, "payouts": 0.9997 } } ``` The component list and values above are illustrative — read the live endpoint for the current picture. A component's `status` is one of `operational`, `degraded`, `partial_outage`, or `major_outage`, and `uptime` is reported as a fraction of operational days over the trailing 90 days. ## The human status page For a quick visual check — component states, incident history, and the 90-day timeline — use the web page at [hannu.africa/status](https://hannu.africa/status). It reads from the same source as the API. > **Automate against the endpoint** > > Point monitoring at GET /v1/status rather than scraping the web page. The endpoint is the machine-readable contract; the web page is for people. --- # Safety and policy Source: https://docs.hannu.africa/platform/policy What Hannu refuses and why — credential-class work blocked in the write path, PII never exposed or logged, and Operator contact never revealed. --- Some of Hannu's most important behaviour is what it won't do. The refusals below aren't a moderation layer on top of the API — they're in the write path, in the same code that creates tasks, so there's no setting that turns them off. ## No credential-class work Hannu refuses **credential-class work** — anything that impersonates a person to a third party or defeats a security control. Account creation, CAPTCHA solving, and one-time-code (OTP) handling are all refused. A task that requests this kind of work is rejected before an Operator ever sees it, returning `safety_filter_blocked` (422). The block sits in the write path, so it can't be configured away. This protects both sides: Operators are never asked to compromise someone's account, and agents can't route abuse through the platform. See [absent by design](/mcp/absent-by-design) for the specific tools Hannu deliberately does not offer. > **Refused in the write path, not after the fact** > > Credential-class requests never reach an Operator. The refusal is structural — the same code path that creates a task rejects the request — not a policy applied later. ## Sensitive identity data is never exposed Operators are KYC-verified, which means Hannu holds sensitive identifiers — BVN, NIN, Tax ID. These are handled under strict rules: - Stored only as a salted hash plus field-level encryption. - Never logged, never displayed, and never placed in an LLM prompt. - Never returned on any developer-facing response. You never see, and never need, an Operator's government identifiers. Verification of identity happens inside the platform; what you receive is the outcome, not the underlying data. ## Operator contact is never revealed An Operator's phone number, email, or other contact detail is never exposed to a developer or an agent. There's no tool or endpoint that returns it. When a task requires contact between you and the field — or between the field and a third party — it's mediated by the platform, so the Operator's contact details stay protected. For visits that need on-site authentication — where the verifier sees the Operator's first name, photo, and tier but never their contact details or government IDs — see [Verify a Pass](/guides/verify-a-pass). ## What you get instead Because identity and contact stay on the platform's side, what you receive is a **public projection** of the work and a **signed Verified report** of what was checked and found — designed to be filed and independently verified. See [how Operators work](/platform/operators). > **Privacy is the default, not a mode** > > There's no configuration that exposes an Operator's identity or contact. The public projection and the signed report are the only surfaces you get — by construction. --- # How Operators work Source: https://docs.hannu.africa/platform/operators Operators are KYC-verified humans who accept task offers and submit proof over a WhatsApp-first workspace — you see a public projection and a signed report, never their contact details or government IDs. --- Behind every task is a real person. Hannu calls them **Operators** — KYC-verified humans who accept offers, do the work in the real world, and submit proof. This page is a conceptual picture of how they work. There's no Operator-facing API to reference here; the Operator surface is not developer-facing. ## Verified before they can work Every Operator completes KYC before they're eligible for any task. The platform holds the sensitive identifiers this requires — and handles them under strict rules: hashed and encrypted, never logged, never displayed, never in a prompt. See [safety and policy](/platform/policy). By the time an Operator can accept an offer, they've been verified; what reaches you is that fact, not the underlying data. ## WhatsApp-first, with a workspace Operators are met where they already are. The primary channel is WhatsApp — offers, instructions, and reminders arrive there — backed by a PWA workspace for the parts of a task that need more than a chat: capturing proof, reviewing task detail, tracking payouts. This is why tasks reach the field quickly and why the experience works on the phones Operators actually carry. ## The task loop, from their side 1. An offer is matched to an eligible Operator, who **accepts** it — emitting `assignment.accepted`. 2. They complete the work in the real world. 3. They **submit proof** — geotagged photos, a form, a recording. Capture timestamps are recorded server-side, not trusted from the device. 4. The proof is verified, and on your approval, escrow releases and the Operator is paid. See [Task lifecycle](/task-types/lifecycle) for the full state machine. ## What you see of an Operator Never their contact details or government IDs. You don't see an Operator's full name, phone number, email, or government identifiers — [Operator contact is never exposed](/platform/policy), and there's no endpoint that returns it. What you get is: - A **public projection** — for a task, its state and progress; for the workers search, an Operator's first name and last initial, languages, coverage, and reputation tier. Enough to match, price, and act, with no contact details or government IDs. - A **signed Verified report** — a statement of what was checked and found, designed to be filed and independently verified. When a visit needs on-site authentication, a [Pass](/guides/verify-a-pass) confirms a real Operator is present under a real task — showing the on-site verifier the Operator's first name, photo, and tier, but never their contact details or government IDs. > **You describe an outcome; a verified human delivers it** > > Your agent asks for a real-world result. A KYC-verified Operator produces it and proves it. The identity stays on the platform's side of the line — you receive the projection and the report, and that's by design.