You have access to Signalc, an API of company buying signals. It reports which companies opened a job requisition, which raised, and which were acquired — each with the posting or the article 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 the date field below date_field occurred_at (default) | posted_at — WHICH DATE THE BOUNDS MEAN. Read the next paragraph before filtering on recency. 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 ON A HIRING ROW, `occurred_at` IS DERIVED AND TELLS YOU NOTHING NEW. It is `posted_at` when the board published one and the read time when it did not, so it is always a copy of one of the other two fields with no way to tell which. Use `posted_at` and `discovered_at`; it is only a fact of its own on funding and acquisition rows, where it is the announcement date against the date we read it. THERE ARE TWO DATES AND THEY ARE NOT THE SAME. `posted_at` is what the company's board published, and is null when the board publishes no date — five of the twenty-four systems we read do not. `occurred_at` is our best answer to when the role opened, and it falls back to when we first read that board. That fallback is accurate for a company we already watch, whose board we read every day. It is not accurate the first time we read a board: its entire backlog arrives in one response, so roles open for years all get today's date. When we start watching a new company you will see a burst of rows dated now. So: filter on `date_field=posted_at` when you mean "the company posted this recently" and want no false positives — undated rows cannot match, which is the point. Leave it alone when you want everything we believe is recent and can tolerate a first read looking new. `posted_at` is on every hiring row so you can always check for yourself. -> { "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, and the three mean different things: - `covered` — we are watching this company's board. Zero rows means nobody is hiring there, and you can say so. - `pending` — we do not know where its board is yet. Zero rows means we have not looked, NOT that the company is not hiring. Ask again shortly. - `unresolved` — we found no public board for it on any applicant-tracking system we read, and we read two dozen. Say that, rather than implying it 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} Four types are produced, and `type=` accepts exactly those four. `hiring` has its own endpoint above; `funding_round`, `acquisition` and `expansion` are served here. Anything else is a 400 naming the valid set rather than an empty page, so a filter you send is a filter that ran. `company_country` is present on roughly one in seven funding and acquisition rows and null on the rest, including every hiring row. Null means we could not establish it, never that the company is elsewhere — so `?country=` narrows to companies we have placed, and is the wrong filter for "everything outside the US". `job_country` on a hiring row is the country of the role and is populated separately. - `funding_round` — payload is { amount, currency, round }. The amount is exactly what the publisher reported and is NOT converted, so two rows in different currencies are not comparable without converting them yourself. `round` is null unless the announcement stated one; never infer it from the amount. - `acquisition` — payload is { amount, currency, acquirer }. `company_name` is the target, the company the event happened to. The buyer is `payload.acquirer`. - `expansion` — the company posted a job in a country it had never posted in before, which is the earliest public sign it is entering that market. Payload is { job_country, job_title, job_url }; `job_country` is the market being entered and the posting is the evidence. A company we have only just started watching never produces one: its existing countries are learned silently, because a board read for the first time shows a decade of hiring all at once. For both, `sources[]` carries the article URL and the publisher. Cite it. `verification_status` is "verified" once a second independent publisher reported the same event, which is worth mentioning when it is true. 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. USE `?q=` TO EXPLORE, THEN SUBSCRIBE WITH THE FILTERS IT RETURNS. Do not put `q=` in a polling loop: the sentence is re-translated on every request, a model is not deterministic, and your subscription can quietly change shape between polls — one call watching one thing, the next watching another, with nothing telling you. Translate once, read `interpreted`, then loop on those filters. `?q=` TAKES A QUESTION IN PLAIN LANGUAGE and translates it into the filters above: GET https://signal-api.rinda.ai/v1/signals?q=which%20German%20companies%20raised%20this%20month The response carries an `interpreted` block naming every filter it produced, and you should read it before reporting the rows — it is how you tell "nothing matched" from "the question was read differently than you meant it". Prefer setting the filters yourself when you already know them; anything you send explicitly wins over the translation. It is a translator, not a search: there is no relevance ranking, and a question these filters cannot express (an industry, a headcount) drops that part and searches wider rather than narrower. ### 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. Three of them, and they answer different questions. posted_at is what the board published and is null when it published none. occurred_at is when we believe the role opened, falling back to when we read the board. discovered_at is when we wrote the row. Rows are ordered by discovered_at; filter recency with date_preset (add date_field=posted_at for published dates only), 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", "posted_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.