API reference

Hiring Signals API

JSON in and out. Three rails share one meter: 6 credits a company scan, plus 1 credit per role when you ask for descriptions. Reading a scan you already paid for is free.

Quickstart

One POST, synchronous. Discovery, the roles and the delta all come back together.

curl -X POST https://hiring.ounie.com/api/scans \
  -H "Authorization: Bearer hrs_live_…" \
  -H "Content-Type: application/json" \
  -d '{"domain":"mercury.com"}'
Read the outcome field before anything else. An outcome of board_not_found means we could not reach a job board for that company. It is not a reading of zero open roles, and it is never billed. Roughly one company in five ends there.

Authentication

  • Your app keyAuthorization: Bearer hrs_live_…, minted at /dashboard/api-keys. Five active keys per account.
  • Your Ounie master key — an ounie_live_… key from ounie.com works across the whole Ounie app fleet, once you switch it on in your ounie.com API-key settings.
  • The session cookie — the dashboard uses the same endpoints you do, signed in on .ounie.com.
  • No auth at all — the x402 rail takes USDC on Base instead.

Hosts that cannot set an Authorization header (the Ounie AI Team’s manual MCP entry, for one) can pass ?api_key=hrs_live_… on any endpoint.

POST /api/scans

Costs 6 credits, or 6 + 1 per described role. Credits are reserved against the real role count, not a guess, and settled on what was produced.

fieldtypemeaning
domainstringThe company's domain. A full URL is fine — we reduce it to the host. Required unless you supply platform + board_token.
platform"greenhouse" | "lever" | "ashby"Skip discovery by naming the board's platform. Must be sent with board_token.
board_tokenstringThe board's own label — `mercury` in job-boards.greenhouse.io/mercury. Requires platform.
include_descriptionsboolean (false)Return each role's description text, the tools named in it, and Ashby compensation. Adds 1 credit per role.
max_role_detailsinteger ≤ 300Cap how many roles get descriptions so a large board cannot surprise you.

The response

{
  "ok": true,
  "scan": {
    "id": "…",
    "domain": "mercury.com",
    "company": "Mercury",
    "outcome": "found",
    "outcome_meaning": "We reached this company's live job board …",
    "headline": "Mercury has 55 open roles. 3 opened, 2 closed since Jul 28, 2026.",
    "platform": "greenhouse",
    "board_token": "mercury",
    "roles": [
      {
        "id": "8403127002",
        "title": "Staff Software Engineer, Payments",
        "team": "Engineering",
        "function": "engineering",
        "seniority": "staff",
        "location": "San Francisco, CA",
        "remote": null,
        "employmentType": null,
        "postedAt": "2026-07-14T09:04:24-05:00",
        "url": "https://job-boards.greenhouse.io/mercury/jobs/8403127002",
        "description": null,
        "salary": null
      }
    ],
    "summary": {
      "totalOpen": 55,
      "byFunction": { "engineering": 21, "sales": 9, "design": 4 },
      "bySeniority": { "senior": 14, "staff": 6, "unknown": 22 },
      "locations": [{ "location": "San Francisco, CA", "count": 18 }],
      "remote": { "yes": 0, "no": 0, "unknown": 55 },
      "toolsMentioned": [],
      "descriptionsIncluded": false
    },
    "delta": {
      "since": "2026-07-28T11:02:19.441Z",
      "previousTotal": 54,
      "netChange": 1,
      "opened": [{ "id": "…", "title": "Solutions Engineer", "function": "sales", "url": "…" }],
      "closed": [{ "id": "…", "title": "Support Lead", "function": "support", "url": "…" }],
      "newFunctions": ["data"],
      "closedFunctions": []
    },
    "discovery": [
      { "platform": "greenhouse", "token": "mercury", "method": "careers-page",
        "status": 200, "jobs": 55,
        "note": "selected — most open roles of the boards that answered" }
    ],
    "roles_detailed": 0,
    "credits_spent": 6
  }
}

Board discovery

You give us a domain; we have to work out which applicant-tracking system that company uses. Three steps, in order:

  • Their own careers page. We fetch /careers, /jobs, /company/careers, /about/careers and a few more, and look for a board link in the markup. This alone resolves about half of companies.
  • A guess from the domain. Most companies name their board after themselves — airtable.com is airtable on Greenhouse. This takes the hit rate to roughly four in five.
  • Probe every candidate in parallel and keep the fullest board. Never the first one that answers. Airtable and Mercury both return 200 on Greenhouse and on Ashby; their Ashby boards are empty shells while the real roles sit on Greenhouse. A first-match design reports zero for both.

Every candidate we probed comes back in discovery[] with its status, its role count and why it did or did not win — so a surprising answer is auditable rather than mysterious.

It is never 100%. Some companies have no keyless board at all. When nothing resolves you get outcome: "board_not_found" and a full refund. If you know where they post, send platform + board_token and we skip discovery entirely.

Outcomes

outcomemeaningbilled
foundWe read the company's live board. `roles` is complete for that board — including when it is empty, which is a real reading of a hiring freeze.6 cr
board_not_foundNo reachable board on any supported platform. We have no reading. Do not render this as zero open roles.0 — refunded in full

The distinction is the whole product. A sales team acting on a fabricated zero is worse off than one with no data at all, so a non-answer is always labelled and never charged for.

The delta

Every successful scan writes a snapshot of that board. The next scan of the same company diffs against it and returns delta.

fieldmeaning
sinceTimestamp of the snapshot we compared against. Always stated, never assumed.
previousTotalHow many roles were open in that earlier reading.
netChangetotalOpen − previousTotal.
opened[]Roles present now and absent then. The buying signal.
closed[]Roles present then and absent now — filled, or pulled.
newFunctions[]Functions with roles now that had none before. The loudest field here.
closedFunctions[]Functions that had roles and now have none.
  • The diff key is the platform’s own job id, so a retitled req is not mistaken for a new one, and two roles with the same title do not collapse into one.
  • The first scan of a board has no delta. You get delta: null and a deltaNote saying so — not a row of zeros that would read as “nothing changed”.
  • The snapshot corpus is per board, not per account. A job board is public, so every rail contributes to it and every rail benefits — including keyless x402 callers, who have no account to keep history against.
  • We prefer a comparison snapshot at least an hour old, so two scans a minute apart do not return a meaningless empty delta. When the only earlier snapshot is more recent than that, we use it and since says so.

Field caveats

Three applicant-tracking systems that agree on almost nothing get normalized into one shape. Where a platform genuinely does not publish something, the field is null and the scan carries a capabilities block saying so — we never infer a value to make the shape look complete.

fieldcaveat
remoteA real boolean on Lever and Ashby, which publish a flag. NULL on every Greenhouse role — Greenhouse location is free text like “Remote - US”, and parsing that into a boolean would be right most of the time, which is not a property you can filter on. A uniform remote filter across all three is not possible.
salaryAshby only, and only when include_descriptions is set. Greenhouse and Lever publish none. Some Ashby boards return the compensation object with every field null, so we return null rather than an empty shell that reads like a measurement.
toolsMentionedExtracted from description text we actually fetched, against a closed vocabulary. Empty unless you bought descriptions — a company's stack is never guessed from job titles.
functionOur classification, from the title, so it is comparable across companies that call the same team different things. Deterministic — no model assigns it.
teamThe platform's own department or team label, verbatim. Null when the platform does not give one.
seniorityFrom the level token in the title. `unknown` whenever there is no token — a bare “Engineer” is not assumed to be mid-level.
descriptionPlain text. Greenhouse ships it as entity-escaped HTML whose inner text carries its own entities, so it is decoded twice before the tags are stripped.

Functions: engineering, product, design, data, security, it, sales, marketing, customer_success, support, finance, legal, people, operations, research, executive, other. Seniorities: intern, junior, mid, senior, staff, principal, lead, manager, director, vp, executive, unknown.

Platforms

Three ship. The property they all share is the one that matters: a board that does not exist returns a clean 404, which is what lets us tell “not on this platform” apart from “not hiring”.

platformendpointmiss
Greenhouseboards-api.greenhouse.io/v1/boards/<token>/departments404 with {"status":404,"error":"Job not found"}
Leverapi.lever.co/v0/postings/<token>?mode=json404 with {"ok":false,"error":"Document not found"}
Ashbyapi.ashbyhq.com/posting-api/job-board/<token>404 with the bare body `Not Found`

And the ones deliberately absent, with the reason for each:

platformwhy not
SmartRecruitersA nonsense company id returns HTTP 200 with {"totalFound":0,"content":[]} — byte-identical to a real company with no openings. There is no way to tell 'not on this platform' from 'not hiring', so shipping it would mean guessing.
WorkableThe widget endpoint discriminates 200/404, but its field shape was never verified against enough real boards to normalize honestly. Absent rather than half-mapped.
RecruiteeNo keyless endpoint that answers reliably for an arbitrary company.
WorkdayEach tenant runs its own host and its own path; there is no keyless, guessable endpoint. A per-tenant integration is not the same product.
SmartRecruiters is the instructive one. A nonsense company id returns HTTP 200 with {"totalFound":0,"content":[]} — byte-identical to a real company with nothing open. There is no way to distinguish them, so shipping it would mean guessing, so it is not in the enum, the docs, or the marketing.

Reading past scans

Free — you already paid for these.

GET https://hiring.ounie.com/api/scans?limit=20
GET https://hiring.ounie.com/api/scans?domain=mercury.com
GET https://hiring.ounie.com/api/scans/<scan_id>

Filtering by domain is how you pull a company’s whole scan history and watch the board move over time.

MCP

Endpoint  https://hiring.ounie.com/api/mcp   (legacy SSE: /api/sse)
Auth      Authorization: Bearer hrs_live_…
          …or https://hiring.ounie.com/api/mcp?api_key=hrs_live_…
toolcostdoes
scan_company_hiring6 crRead a company's board and diff it against the last reading.
get_hiring_scanfreeRetrieve a scan by id.
list_hiring_scansfreeList past scans, optionally for one domain.
get_coveragefree · publicWhich platforms are read, which are refused and why, how discovery works, and every field caveat.
get_credit_balancefreeSpendable Ounie credits.
get_pricingfree · publicCredit and x402 pricing.
whoamifreeThe key's owner metadata.
Call get_coverage before you interpret a result. An absent company is not a company that is not hiring, and an agent that does not know the difference will report one as the other.

x402 — keyless, pay per call

Agents with no Ounie account pay in USDC on Base. Because the job boards cost us nothing to read, discovery runs before the quote.

curl -X POST https://hiring.ounie.com/api/x402/scan \
  -H "Content-Type: application/json" \
  -d '{"domain":"mercury.com"}'

# → 402
{ "x402Version": 1,
  "accepts": [{ "scheme": "exact", "network": "base",
                "maxAmountRequired": "72000",
                "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
                "payTo": "0x…",
                "extra": { "name": "USD Coin", "version": "2" } }],
  "preview": { "company": "Mercury", "platform": "greenhouse",
               "open_roles": 55, "credits_equivalent": 6 } }

curl -X POST https://hiring.ounie.com/api/x402/scan \
  -H "X-Payment: <base64 signed payload>" \
  -d '{"domain":"mercury.com"}'
  • A company with no reachable board is a 400, with no price in the body. You never sign an authorization for work that cannot be delivered. The response carries the full discovery[] trail and a hint to retry with a board token.
  • The 402 quotes the exact final amount, descriptions included — not a ceiling. On this rail you cannot settle for less than you quoted (the exact scheme compares the signed value, so a reduced settlement is refused after the work is done), and quoting exactly removes the failure mode.
  • Verify → run → settle. Your signature is checked off-chain first, the board is read, and only then does money move. Nothing is settled for a result we could not produce.
  • The 402 also carries a free preview: the company, the platform and the open-role count, so you can decide whether the answer is worth $0.072 before signing anything.
  • x402 scans still get a delta. The snapshot corpus is keyed by board, not by account, so a keyless caller both reads from it and contributes to it.

Errors

statusbodymeaning
400invalid_domain / unsupported_platform / board_token_requiredMalformed request. Refused before any price is quoted.
400board_not_found (x402 only)No reachable board. Free, with the discovery trail attached, and no quote.
401unauthorizedMissing or bad Bearer / cookie. Never a redirect — these routes are outside the proxy matcher.
402insufficient_creditsCarries required_credits, balance_credits and buy_credits_url (https://ounie.com/dashboard/settings).
402partial_delivery (x402 only)Descriptions came back short of the quote after a retry. Nothing was settled.
429too_many_runningMore than 5 scans in flight for this account.
502scan_failedThe scan could not be completed. Credits were refunded pool-exact; on x402 nothing was settled.

Limits

limitvalue
Roles normalized per board1000
Role descriptions per scan300
Concurrent scans per account5
Active API keys per account5
Scan price6 credits · $0.072 on x402
Role description price1 credit each

We self-throttle and cache board discovery per domain. None of the three platforms publishes a rate-limit header, so being courteous is on us rather than on them.