You have access to Signalc, an API of company hiring events. It reports which companies opened a job requisition, in which function, and how senior — with the job posting attached as proof. It does NOT predict what a company will buy. That depends on what the user sells, which only they know. A Staff Platform Engineer req is a strong signal to someone selling developer infrastructure and irrelevant to someone selling legal services. Ask the user what they sell, map that to the departments and seniorities that would own the budget, and filter on those. API base: https://signal-api.rinda.ai Reference: https://signal-api.rinda.ai/v1/docs human-readable OpenAPI spec: https://signal-api.rinda.ai/v1/docs/json machine-readable, generated from the routes Fetch the spec if you want the exact parameter list, enum values and response shapes. It is generated from the route definitions, so it cannot drift from what the server actually accepts. This brief covers the parts a spec cannot: which filters matter for which question, and what the numbers do not mean. ## The typical request The user has a list of companies they sell to. Ask for exactly those: GET https://signal-api.rinda.ai/v1/signals/hiring?company_domain=stripe.com,ramp.com&seniorities=vp,director ## Get a key (once, needs a human) 1. POST https://signal-api.rinda.ai/v1/device/code body: {"client_name": ""} returns: device_code, user_code, verification_uri_complete, interval 2. Show the human verification_uri_complete and ask them to open it, create an account, and approve the code shown. Do not ask them for a password or paste a key into this conversation. 3. Poll POST https://signal-api.rinda.ai/v1/device/token body: {"device_code": ""} 202 means keep waiting — wait `interval` seconds between polls. 200 returns {"api_key": "sk_..."}. It is returned exactly once. Store it in the environment as SIGNALC_API_KEY; never print it or commit it. ## Call the API Send `x-api-key: $SIGNALC_API_KEY` on every request (`Authorization: Bearer` also works). ### Hiring signals — one row per job requisition GET https://signal-api.rinda.ai/v1/signals/hiring company_domain THE ACCOUNT FILTER. Strict match, repeatable or comma-separated, max 100. Use it whenever the user has a target account list. Asking about a company we have not seen adds it to our directory, so it is covered from the next cycle. count true returns only how many match. Costs no credits. seniorities intern | entry | mid | senior | lead | director | vp | c_level departments substring of the job function, e.g. engineering search free text across job title and company name countries ISO 3166-1 alpha-2, comma-separated. exclude_countries also works. job_countries country of the role itself, not the company verification unverified | verified | pending date_preset today | last_7d | last_14d | last_30d | last_90d | last_180d | last_365d — relative, and takes precedence over dateFrom/dateTo dateFrom/dateTo ISO 8601 bounds on occurred_at sort_by occurred_at | discovered_at | company_name sort_order asc | desc page / limit 1-based page, limit 1-200 (default 50) cursor "start", then pagination.nextCursor — walks every signal once cursor continue that walk -> { "success": true, "data": [ ... ], "pagination": { "currentPage", "totalPages", "totalCount", "hasNextPage", "hasPreviousPage", "nextCursor" }, "meta": { "endpoint", "creditsUsed", "coverage" } } READ meta.coverage BEFORE REPORTING AN EMPTY RESULT. It splits the domains you asked for into covered / pending / unresolved. Zero rows with a domain in `pending` means we only just started watching that company, not that nothing is happening there. `unresolved` means it has no public board on Greenhouse, Lever or Ashby — say so rather than implying the company is not hiring. ### The merged feed — every signal type, for staying in sync GET https://signal-api.rinda.ai/v1/signals?cursor=...&type=hiring&domain=...&wait=30 GET https://signal-api.rinda.ai/v1/signals/{uuid} An unknown enum value is a 400 listing the valid set. It is never ignored, because silently dropping a filter would return companies nobody asked about. ### Subscribing There are no webhooks. To watch for new signals, hold a request open against the filter you care about: GET https://signal-api.rinda.ai/v1/signals/hiring?company_domain=acme.com&cursor=start&wait=50 It returns the moment something matches, or an empty page when the budget runs out. Boards are read once a day, so signals arrive in one burst per cycle rather than a trickle — a held request beats a polling loop, but nothing appears between cycles. occurred_at is the date the company published the req, not the date we read it, so a role found a day later is still dated the day it opened. Either way pass the nextCursor back and call again — an empty page hands your own cursor back, so the loop never loses its place. That loop is the subscription, and the cursor is the only state you need to keep. Nothing is lost if your process restarts or is down for a day; you resume where you stopped. ## Contracts that matter PAGINATION IS EXACTLY-ONCE. Pass next_cursor back as ?cursor= and you will see every signal once, forever, even while new ones are being written. Never use an offset. A malformed cursor returns 400 — do not respond by restarting without a cursor, that replays the entire history as if it were new. NOTHING HERE RANKS BY RELEVANCE. There is no score, on purpose: a number computed by us cannot know what the user sells, so it would rank by seniority and freshness while implying it ranked by fit. Rank the results yourself using what the user told you they sell. `seniorities` and `departments` are where that knowledge goes. EVERY SIGNAL HAS SOURCES. Each row carries sources[] of {url, fetched_at}. When you tell a human about a signal, link the url so they can open the posting. An unsourced claim is indistinguishable from one you invented. TIMESTAMPS. occurred_at is when the event happened; rows are ordered by when we learned of it. Filter recency on occurred_at, paginate on the cursor. RATE LIMITS. trial 30/min, growth 300/min, scale 1200/min. Read x-ratelimit-remaining and back off on 429 using retry-after. ## Shape of a hiring signal { "id": "019fa816-d5fd-700a-9ef8-06c9566e6420", "signal_type": "hiring", "company_name": "Linear", "company_domain": "linear.app", "company_country": "US", "occurred_at": "2026-07-28T09:38:16.114Z", "discovered_at": "2026-07-28T09:38:16.702Z", "verification_status": "unverified", "title": "Implementation Manager", "job_url": "/v1/jobs/019fa816-d5fd-700a-9ef8-06c9566e6420", "location": "North America", "city": "North America", "job_country": "US", "employment_type": "FullTime", "seniority_level": "lead", "job_function": "GTM", "sources": [ { "url": "/v1/jobs/019fa816-d5fd-700a-9ef8-06c9566e6420", "fetched_at": "..." } ] } job_url and sources[].url are redirects on this API — follow one and you land on the live posting. Where a posting is hosted is not published; do not speculate about it to the user. seniority_level is inferred from the title, not published by the board. Treat it as a filter, not as fact. Fields the boards do not publish — applicant counts, company headcount, industry — are absent rather than guessed. ## MCP The same API is an MCP server at https://signal-api.rinda.ai/mcp (Streamable HTTP), authenticated with the same x-api-key header. Tools: search_signals(type?, country?, since?, limit?) and get_signal(id). Use it instead of raw HTTP if your runtime speaks MCP. Manage keys and try requests in a browser at https://signal.rinda.ai.