API REFERENCE · v1

Everything you need to build on FetchTalos.

Every endpoint below is real and live on this server — discovery, hiring, payroll, insurance, and hub self-service. No placeholders, nothing aspirational: what's documented here is what the API actually does today, including the honest edges.

Base URLhttps://fetchtalos.onrender.com
AuthAuthorization: Bearer <key>
FormatJSON in / JSON out

Overview

FetchTalos is one API across four jobs that used to need four different vendors: finding talent, generating a compliant contract, running payroll with live FX conversion, and issuing health coverage tied to that same contract. All of it is scoped by API key, so an enterprise and a hub calling the same base URL only ever see their own data.

i
Every request/response shown on this page is the real shape returned by the current server — copied from the actual route handlers, not written from a spec. If a field is nullable or only appears under certain conditions, that's called out explicitly.

Authentication

Every /v1/* route (except the talent-facing public links) requires a bearer key.

Header
Authorization: Bearer ft_test_51x9k2mq7dev
Key types
enterprise
Sees the full shared talent pool across every hub pipeline. This is what a direct hiring employer uses.
hub
Locked to one pipeline via hub_scope (e.g. "ALX Africa"). Talent discovery, roster management, and self-service account routes are all silently scoped to that pipeline — a hub key can never read or write another hub's data, regardless of what a request body claims.
!
Keys aren't self-service yet — there's no public signup. Request access and FetchTalos issues you a key directly.

Errors

Errors are always JSON with an error slug and usually a human-readable message. There's no generic catch-all shape beyond that — each route returns the slug relevant to what actually went wrong.

401 unauthorized
Missing or invalid bearer key.
403 hub_only
Route is restricted to hub-type keys (roster management, hub self-service).
404 *_not_found
Resource doesn't exist, or exists but isn't scoped to your key — both return the same 404, deliberately, so you can't probe for other clients' data.
409 conflict
Action doesn't make sense given current state — e.g. re-sending a contract that's already out, releasing an already-released contract.
422 not_configured
The route depends on an integration (insurance, NGN rail) that isn't configured server-side yet.
502 upstream failure
A downstream call (FX lookup, insurance provider) failed. Safe to retry.

Talent Discovery

Browse the shared talent pool. A hub key only ever sees its own pipeline; an enterprise key sees everyone.

GET/v1/talents/discoverany key
Search available talent. Hub-scoping is automatic and can't be overridden by query params.
Query parameters
ParamTypeReqDescription
skillstringnoSubstring match against each talent's stack (case-insensitive) — e.g. ?skill=node.
statusstringnoExact match on talent status, e.g. available.
regionstringnoCountry match. Best-effort — falls through to unfiltered until pipeline data carries city-level granularity.
Request
curl "$BASE_URL/v1/talents/discover?skill=node" \
  -H "Authorization: Bearer $API_KEY"
200 Response
{
  "count": 1,
  "results": [{
    "talent_id": "tal_0x91af",
    "name": "Chinedu O.",
    "stack": ["Node", "Postgres", "Go"],
    "pipeline": "ALX Africa",
    "country": "NG",
    "vetted_score": 92,
    "status": "available"
  }],
  "scoped_to_hub": null
}

The Hiring Flow

Hiring is a real handshake, not one instant call — the talent has to actually confirm at two separate points before a contract goes active. Five steps, two of which are talent-facing public links (no API key, token-based, the same pattern as a DocuSign/Calendly confirmation link).

1 · POST /v1/engagements/create
Enterprise sends an interview invite. Terms — currency, coverage plan, proposed salary — are captured here and carried through the rest of the flow; nothing is re-entered later.
2 · GET /v1/engagements/:id/accept (public, talent-facing)
Talent clicks the link in their email to confirm they're in for the interview.
3 · Interview happens off-platform
On whatever call tool was in the invite. FetchTalos isn't a video product.
4 · POST /v1/engagements/:id/contract
Enterprise moves forward. A real offer letter PDF is generated and emailed to the talent along with an accept link.
5 · GET /v1/contracts/:id/accept (public, talent-facing)
Talent accepts. Only now does the contract flip to active — insurance gets purchased, a salary/NUBAN account gets issued, and the first pay period opens.
POST/v1/engagements/createany key
"Engage" — sends the talent an interview invite email and locks their status to interviewing so nobody else can engage them mid-process.
Body parameters
ParamTypeReqDescription
talent_idstringyesMust be currently available.
employer_namestringyesCompany name — appears on the offer letter.
employer_emailstringyesReal HR/finance contact — this is who gets payment-cycle and due-payment reminder emails.
role_titlestringyese.g. "Backend Engineer".
kpisstring[]yesAt least one non-empty KPI string.
interview_linkstringyesCalendly/Zoom/Meet — whatever the enterprise uses.
employer_countrystringnoDrives which tax form applies (e.g. US → W-8BEN).
employer_currencystringnoDefaults to USD. Use NGN for the domestic rail.
coverage_planstringnoDefaults to remote_contractor_basic. See Insurance Plans.
coverage_monthsnumbernoDefaults to 1 — how often the coverage cycle renews.
proposed_amountnumbernoMonthly salary in employer_currency.
proposed_time / messagestringnoShown in the invite email as-is.
Request
curl -X POST "$BASE_URL/v1/engagements/create" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "talent_id": "tal_0x91af",
    "employer_name": "Acme Inc",
    "employer_email": "hr@acme.com",
    "role_title": "Backend Engineer",
    "employer_country": "US",
    "employer_currency": "USD",
    "proposed_amount": 4200,
    "interview_link": "https://cal.com/acme/interview",
    "kpis": ["Ship the v1 API redesign"]
  }'
201 Response
{
  "engagement_id": "eng_4a1c9b02",
  "status": "interview_invited",
  "talent_name": "Chinedu O.",
  "employer_name": "Acme Inc",
  "accept_url": "https://.../v1/engagements/eng_4a1c9b02/accept?token=..."
  // ...plus every field submitted above
}
!
Returns 422 talent_missing_email if the talent record has no email on file — engaging requires somewhere to send the invite.
GET/v1/engagementsany key
Lists every engagement created by your key. Returns { count, results }.
GET/v1/engagements/:id/acceptpublic — no key
Talent-facing confirmation link, validated by a ?token= query param rather than a bearer key. Flips the engagement to interview_accepted. Returns an HTML confirmation page, not JSON.
POST/v1/engagements/:id/contractany key
Sends contract terms after the interview. Generates a real offer letter PDF (role, remuneration, KPIs, benefits) via pdfkit and emails it to the talent along with an accept link. No body required — terms come from the engagement itself.
Request
curl -X POST "$BASE_URL/v1/engagements/eng_4a1c9b02/contract" \
  -H "Authorization: Bearer $API_KEY"
201 Response
{
  "contract_id": "ctr_7f02ab13",
  "status": "pending_talent_signature",
  "tax_form": "W-8BEN",
  "coverage_status": "not_yet_purchased",
  "accept_url": "https://.../v1/contracts/ctr_7f02ab13/accept?token=...",
  "offer_letter_attached": true
}
!
409 contract_already_sent if a contract for this engagement was already generated — this route is not idempotent-retry-safe by design; a second call would issue a second offer letter.
GET/v1/contracts/:id/acceptpublic — no key
The other talent-facing link, token-validated. If required KYC fields are still missing (bank account, BVN/NIN, phone, depending on rail), this renders an HTML form instead of finalizing — the matching POST /v1/contracts/:id/accept handles that form's submission. Once nothing's missing, this is what actually flips the contract to active, purchases coverage, issues the talent's salary account, and opens pay period #1.

Contracts

Once a contract is active, these are the read/manage routes you'll use day to day.

GET/v1/contractsany key
Lists every contract scoped to your key. Each active contract with unreimbursed insurance is enriched with a live FX estimate of that premium.
200 Response (excerpt)
{
  "count": 1,
  "results": [{
    "contract_id": "ctr_7f02ab13",
    "status": "active",
    "employer_currency": "USD",
    "coverage_status": "active",
    "insurance_due_next_payment": true,
    "insurance_estimate_employer_currency": 14.2,
    "insurance_estimate_fx_source": "live:open.er-api.com"
  }]
}
GET/v1/contracts/:idany key
The full contract object — coverage status, salary/NUBAN details, tax form, KYC status, everything. The single source of truth for "what does this contract currently look like."
GET/v1/contracts/:id/pay-periodsany key
The recurring salary schedule for this contract. Status (due / overdue / paid) is computed live on every read from the due date, not stored — no scheduler needed to keep it accurate.
GET/v1/contracts/:id/amount-dueany key
The exact total to wire this cycle — NGN-flow contracts only (400 ngn_flow_only otherwise). Uses the identical calculation the real settlement uses internally, so the number shown here and the number actually charged can never drift apart.
200 Response
{
  "contract_id": "ctr_9c21ff44",
  "salary_naira": 450000,
  "service_fee_naira": 22500,
  "platform_fee_naira": 22500,
  "hub_markup_naira": 0,
  "insurance_due_this_cycle": true,
  "insurance_naira": 8500,
  "insurance_is_live_quote": true,
  "total_naira": 481000,
  "pay_to_account_number": "9012345678",
  "pay_to_bank_name": "Rubies MFB",
  "last_payment": null,
  "next_payment_due_date": "2026-09-29T00:00:00.000Z"
}
i
service_fee_naira is platform_fee_naira + hub_markup_naira combined deliberately — an enterprise only needs one honest total, not a breakdown that reads like two different parties are each taking a cut. Hubs can see their own markup broken out via GET /v1/hub/account and GET /v1/hub/stats.
POST/v1/contracts/:id/simulate-depositany key
NGN-flow contracts only, and only meaningful in test mode. Simulates the employer's own wire landing on the talent's real intermediate NGN balance via Felicity's simulate_funding action — which fires a real webhook back to this server asynchronously and triggers the actual settlement pipeline (insurance, fee/markup split, salary send). The response returns before that settlement has happened.
Body parameters
ParamTypeReqDescription
amount_nairanumbernoDefaults to the contract's own proposed_amount.
POST/v1/contracts/:id/releaseany key
Ends the relationship — works whether the contract was accepted or is still pending signature. Any unpaid pay periods are voided, the talent's status flips back to available so they reappear in discovery, and the contract becomes terminal. Money already paid stays paid; this only affects what's still ahead.
GET/v1/contracts/:id/w8benany key
Downloads the real, signed IRS Form W-8BEN PDF directly from Dropbox Sign — US-employer contracts only, and only once w8ben_status is signed. Nothing is cached locally; every call fetches the current signed document live.

Payroll & Ledger

Employer pays in their own currency, talent receives NGN — conversion happens against a live rate at disbursement time.

POST/v1/payroll/disburseany key
Runs payroll for one or more contracts in a single call. Amounts are always derived from each contract's own proposed_amount — never accepted from the request — so what's disbursed always matches what the talent was actually promised.
Body parameters
ParamTypeReqDescription
contract_idsstring[]yes*Contracts to run payroll against. *contract_id (singular string) also accepted for back-compat with the older single-contract shape.
idempotency_keystringnoApplied per-contract as key:contract_id. Replaying the same key returns the original payout instead of paying twice — always derive this from something stable on your side (e.g. payroll_run_id + contract_id), never a random value.
Request
curl -X POST "$BASE_URL/v1/payroll/disburse" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "contract_ids": ["ctr_7f02ab13"],
    "idempotency_key": "payroll-2026-09-run"
  }'
202 Response
{
  "count": 1, "succeeded": 1, "failed": 0,
  "totals": { "total_gross": 4200, "total_platform_fee": 210, "total_employer_charged": 4410 },
  "results": [{
    "payout_id": "pay_c81f0e22",
    "fx_rate": 1580.42,
    "fx_source": "live:open.er-api.com",
    "net_amount_ngn": 6637764,
    "employer_total_charged": 4410,
    "status": "settled"
  }]
}
!
This is the USD/GBP/EUR/CAD rail, and it will reject an NGN-flow contract outright with wrong_rail_for_ngn — NGN contracts settle automatically via Felicity's webhook (see simulate-deposit), never through this endpoint. Currently, this endpoint's fund movement is simulated within the API for demo/testing purposes — it doesn't yet call a live Fincra/Rubies transfer for non-NGN currencies. fx_rate is real and live; the transfer itself is the part still ahead of a licensed money-movement integration for these currencies.
GET/v1/payroll/:idany key
Fetch a single payout by its payout_id.
GET/v1/ledgerany key
Aggregate payout history, scoped to your key — running volume by currency, total platform revenue, and the full payout list (newest first).
GET/v1/ratesany key
The same live FX rate payroll disbursement actually uses, exposed so you can compute an honest estimate instead of guessing.
Query parameters
ParamTypeReqDescription
currencystringnoDefaults to USD.
200 Response
{ "currency": "USD", "rate": 1580.42, "source": "live:open.er-api.com" }

Insurance Plans

Health coverage is tied directly to the contract, priced against the live catalog.

GET/v1/plansany key
Lists the three coverage plans (basic / plus / family) with their catalog base premium and benefits, live from the insurance provider. Returns 422 not_configured if the NGN insurance rail isn't set up server-side yet.
GET/v1/plans/quoteany key
The real premium for a specific plan + duration, quoted live — not the catalog's static base price.
Query parameters
ParamTypeReqDescription
planstringyese.g. remote_contractor_basic.
monthsnumbernoDefaults to 12.

Hub Self-Service

Everything a hub needs to run its own white-labeled pipeline — markup rate, settlement account, notification email, roster, and stats. All routes below return 403 hub_only for enterprise keys, and every write is silently forced into the caller's own hub_scope — a hub can never touch another hub's data no matter what a request body says.

GET/v1/hub/accounthub key only
Current settings: markup rate, admin-set cap, settlement account, notification email, scope.
PATCH/v1/hub/markuphub key only
Sets the hub's own white-label markup rate — always clamped server-side to the admin-set ceiling, even if a higher value is sent.
Body parameters
ParamTypeReqDescription
markup_bpsnumberyesBasis points, e.g. 350 = 3.5%. 400 exceeds_cap if above your ceiling.
PATCH/v1/hub/settlement-accounthub key only
Where this hub's markup gets paid on every settlement.
Body parameters
ParamTypeReqDescription
account_numberstringyes
account_namestringyes
bank_codestringyesCBN bank code.
PATCH/v1/hub/notification-emailhub key only
Where "your talent was just paid, here's your markup" notifications get sent. Without this set, a hub simply never gets notified — silently.
Body parameters
ParamTypeReqDescription
emailstringyes
GET/v1/hub/statshub key only
Real activity numbers for this hub's pipeline only — talent count, active/total contracts, distinct enterprises engaged, payments settled, and total salary + markup paid out. Totals come from a real per-settlement ledger, not a snapshot, so a talent paid multiple times is counted correctly.
GET/v1/talents/rosterhub key only
A hub's own full roster — every status, not just available. Distinct from /discover, which is the enterprise-facing "who can I hire right now" view; this is the hub-facing "everyone I've ever uploaded" view.
POST/v1/talents/uploadhub key only
Bulk create/update, deduped by email within your own pipeline. Each item is processed and reported independently — one bad row in a 40-talent sync doesn't fail the other 39.
Body parameters
ParamTypeReqDescription
talentsobject[]yesEach item: { name, email, country } required, plus optional stack (string[]) and vetted_score (number, defaults 75).
200 Response
{ "count": 2, "created": 1, "updated": 1, "failed": 0, "results": [ /* one entry per input item, each with action: "created"|"updated" or an error */ ] }
!
An email that already exists under a different hub's pipeline is rejected as email_conflict for that item — nothing gets silently overwritten across pipelines.
PATCH/v1/talents/:idhub key only
Edit one talent in your own roster. name, stack, country, vetted_score are editable; pipeline and status are not.
DELETE/v1/talents/:idhub key only
Remove a talent from your roster. Blocked with 409 contract_in_progress if they have a pending or active contract — the enterprise needs to release it first.

System

GET/healthpublic — no key
Liveness check. Also reports whether persistence (Redis-backed state across restarts) is currently enabled.
200 Response
{ "ok": true, "service": "fetchtalos-api", "time": "2026-08-30T12:00:00.000Z", "persistence": "enabled" }