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
Param
Type
Req
Description
skill
string
no
Substring match against each talent's stack (case-insensitive) — e.g. ?skill=node.
status
string
no
Exact match on talent status, e.g. available.
region
string
no
Country match. Best-effort — falls through to unfiltered until pipeline data carries city-level granularity.
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
Param
Type
Req
Description
talent_id
string
yes
Must be currently available.
employer_name
string
yes
Company name — appears on the offer letter.
employer_email
string
yes
Real HR/finance contact — this is who gets payment-cycle and due-payment reminder emails.
role_title
string
yes
e.g. "Backend Engineer".
kpis
string[]
yes
At least one non-empty KPI string.
interview_link
string
yes
Calendly/Zoom/Meet — whatever the enterprise uses.
{
"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"
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.
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.
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
Param
Type
Req
Description
amount_naira
number
no
Defaults 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
Param
Type
Req
Description
contract_ids
string[]
yes*
Contracts to run payroll against. *contract_id (singular string) also accepted for back-compat with the older single-contract shape.
idempotency_key
string
no
Applied 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.
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.
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
Param
Type
Req
Description
plan
string
yes
e.g. remote_contractor_basic.
months
number
no
Defaults 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.
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
Param
Type
Req
Description
markup_bps
number
yes
Basis 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
Param
Type
Req
Description
account_number
string
yes
—
account_name
string
yes
—
bank_code
string
yes
CBN 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
Param
Type
Req
Description
email
string
yes
—
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
Param
Type
Req
Description
talents
object[]
yes
Each 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.