API reference
MCP
The Signals MCP server. One HTTP POST speaks the MCP streamable-HTTP protocol and answers with a server-sent event stream. GET, PUT, PATCH and DELETE answer 405 with Allow: POST, even when the key is missing and even while the API is closed. POST without a key, or with an invalid, revoked or ejk_ key, is 401. A valid gtk_ key while the Signals API is closed answers 403 api_not_enabled.
The assistant server at /api/mcp (key prefix ejk_) is a different server. Connect it from Connect your assistant. An ejk_ key cannot call this URL.
Authentication
Send Authorization: Bearer gtk_…. Create the key under Connect assistant → API keys. A missing, invalid or revoked key is 401. A valid key while this API is not enabled for your organization answers 403 api_not_enabled, with what the key can do instead. Every response includes mySDR-Api-Version, mySDR-Request-Id and Cache-Control: no-store, and a response to an accepted key also X-RateLimit-* (so not a 401, a 405, an OPTIONS or a 404 for an unknown path). Send Accept: application/json, text/event-stream and a JSON-RPC body; the response is an SSE stream, not a single JSON body (read the JSON on its data: line), and keeps Cache-Control: no-cache, no-transform. A tools/call refused by the rate limit answers plain JSON instead (a tool error with isError that says when to retry), so read the Content-Type. The server is stateless: tools/list and tools/call work without initialize, which an MCP client library sends anyway.
Body parameters
jsonrpcstring· required — Always2.0. One of:2.0.idinteger | string— Your id for the request; the response carries it back. Leave it out on a notification such asnotifications/initialized.methodstring· required —initialize,tools/list,tools/call, or a notification.paramsobject— Fortools/call:name(the tool) andarguments(its input). Forinitialize:protocolVersion,capabilitiesandclientInfo.
Response
event streamtext/event-stream· required — MCP JSON-RPC messages. Atools/callresult carries the REST body plusdashboard_urlwhere the tool has one.
Credits
The HTTP call is free. A tool spends the credits of the REST endpoint it calls. preview_cost, lookup_company, get_search_results, list_mentions and manage_monitor spend nothing.
Errors
- 400
-32700— The body is not JSON, or not a JSON-RPC message. - 405
-32000— GET, PUT, PATCH and DELETE answer a JSON-RPC error andAllow: POST, before the key is checked and while the API is closed. POST without a key is 401. - 406
-32000— The request does not accept bothapplication/jsonandtext/event-stream(theAcceptheader). - 413
-32000— The body is larger than 512 KB. - 415
-32000— TheContent-Typeis notapplication/json. - 401
authentication_error— The Authorization header is missing, or the key is unknown or revoked. - 403
permission_error— API access is not part of your plan, or your account is suspended. - 403
api_not_enabled— The key is valid, but the Signals API or the Company API is not enabled for your organization yet. The error says what this key can do. - 429
rate_limit_error— More than 60 requests per minute, or more than 3 at the same time, on one key. Wait for the Retry-After header. - 500
internal_error— Something went wrong on our side.
Example
cURL
curl -X POST "https://mysdr.app/api/v1/mcp" \
-H "Authorization: Bearer $MYSDR_API_KEY" \
-H "Accept: application/json, text/event-stream" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list"
}'
JavaScript
const response = await fetch('https://mysdr.app/api/v1/mcp', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.MYSDR_API_KEY}`,
'Content-Type': 'application/json',
"Accept": "application/json, text/event-stream"
},
body: JSON.stringify({
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list"
})
});
const text = await response.text();
const body = response.headers.get('content-type')?.includes('text/event-stream')
? JSON.parse(text.split('\n').find((line) => line.startsWith('data: ')).slice(6))
: JSON.parse(text);
Python
import json
import os
import urllib.request
payload = {
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list"
}
request = urllib.request.Request(
"https://mysdr.app/api/v1/mcp",
headers={
"Authorization": f"Bearer {os.environ['MYSDR_API_KEY']}",
"Content-Type": "application/json",
"Accept": "application/json, text/event-stream"
},
data=json.dumps(payload).encode(),
method="POST",
)
with urllib.request.urlopen(request) as response:
text = response.read().decode()
if "text/event-stream" in response.headers.get("Content-Type", ""):
body = json.loads(next(line[6:] for line in text.splitlines() if line.startswith("data: ")))
else:
body = json.loads(text)
Example response
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [
{
"name": "lookup_company"
},
{
"name": "enrich"
},
{
"name": "preview_cost"
},
{
"name": "search_signals"
},
{
"name": "get_search_results"
},
{
"name": "company_leads"
},
{
"name": "create_monitor"
},
{
"name": "list_mentions"
},
{
"name": "manage_monitor"
}
]
}
}
preview_cost and max_credits
preview_cost is free. It does not call a source. Send endpoint (searches, leads, person.enrich or monitors) and body, the JSON the real call would use. The result is a cost_preview: credits_per_row, max_rows, max_credits and period (call or day). The REST equivalents are POST /searches/preview, POST /leads/preview, POST /person/enrich/preview and POST /monitors/preview.
max_credits is an integer cap on the real call. The call stops before the next row would pass it. That row is left out (truncated_by max_credits) and is not charged. Below the cost of one row the call is 402 max_credits_exceeded and nothing runs. It works with every key. A monitor uses max_credits_per_day instead.
Tools
The server name is mysdr. tools/list returns these nine tools. Each one calls the REST endpoint below with your gtk_ key. A result that has dashboard_url opens the same search, monitor or people in mySDR (/en/gtm/leads or /en/gtm/signals).
lookup_company
Free. Input: name (the company name, or a LinkedIn company page URL). Output: object list with the pages whose name matches in data, or an empty data and a message. REST: GET /companies/lookup, same body.
enrich
A profile is 1 credit when one is found. include_email: true adds 1 when an address is found. A company is 1 credit when one is found. A miss is free. There is no phone field: a phone number is Find Phone (POST /person/phone). Input: exactly one of profile_urls, person_ids (the per_ rows of search_signals and company_leads), or company domains / linkedin_urls / names. Optional: include_email (a person), fields and exact_match (a company), max_credits. Output: the enrich body for that person or company. REST: POST /person/enrich or POST /company/enrich.
preview_cost
Free. Input: endpoint and body (the JSON of the real call; max_credits goes inside body). Output: cost_preview. REST: the /preview route for that endpoint.
search_signals
The rows of one signal: what the source shows, with no AI judging them. A person from engagement, listening, an event or a new role is 1 credit, a funding company 1 credit, a technology company 3. A search that finds nobody costs nothing, and the same arguments on the same day return the same search without charging again. Input: signal (every type), optional limit, cursor (the result.next_cursor of an earlier answer, with the same signal), exclude_companies and max_credits. It waits up to 25 seconds and reports progress. Output: result (rows, has_more, next_cursor), the first 25 rows in results, next_page_cursor for the rest, notes, funnel and dashboard_url; still running: search_id and a poll hint. A LinkedIn row is a person with their headline and up to 3 things they did, each with the post: the assistant decides which rows fit, then enriches those with person_ids. An event is never looked up: send its exact name with the year, its hashtags or its organizer page. To follow a signal over time, call it again later, for example every week with a short lookback_days. REST: POST /searches, then GET /searches/{search_id}/results.
get_search_results
Free. Input: search_id (srch_ from search_signals), optional page_cursor (the next_page_cursor of the previous page). Output: the search's status (queued or running: no rows yet and a poll hint), then 25 rows per page in data, next_page_cursor (null = no more), and on the first page result, funnel and notes, plus dashboard_url. Also reads a search that was still running when search_signals answered. REST: GET /searches/{search_id} and GET /searches/{search_id}/results.
company_leads
1 credit per person. A company that does not match costs nothing. Input: companies (each with one of id, domain or linkedin_url), filters (titles, seniority, departments, countries; roles is the earlier name of titles), optional per_company, limit, cursor and max_credits. Output: per_ rows and dashboard_url. REST: POST /leads.
create_monitor
Creating it is free. Each new mention on a run is 1 credit. Input: name, keywords, optional schedule (daily; hourly stays off unless that deployment has turned it on), min_relevance, tags, max_credits_per_day. A keyword can carry the settings of Create a monitor. They are checked on the text of each post the search found, so they can only drop posts. Output: the monitor and dashboard_url. REST: POST /monitors.
list_mentions
Free. Input: optional monitor_id (mon_), min_relevance and cursor (the next_cursor of the previous page). Output: the mentions already delivered in data (an empty list when there are none), each with the keywords it counts for and its author (author_name, author_linkedin_url, author_headline), plus has_more and next_cursor. REST: GET /mentions.
manage_monitor
Free. Input: action (list, get, update, pause, resume or delete) and, for every action except list, monitor_id. update takes name, keywords (the whole new list), max_credits_per_day (or remove_cap: true for no cap; null means "not given", as in every tool), min_relevance, tags and schedule; what you leave out stays. list takes cursor. Output: the monitor (or the list) and dashboard_url. delete stops the monitor for good; its mentions stay readable for 30 days. REST: GET /monitors, GET, PATCH and DELETE /monitors/{monitor_id}.