REST API Reference
Use these entry points to work with the Medialyst API.
1. Get An API Key
Create API keys from the Medialyst Developers page. The _ segment resolves to your current organization.
Give each key a clear name, choose the smallest useful scopes, copy the raw key immediately, and store it in secret storage or an environment variable such as MEDIALYST_API_KEY.
Send the key as a bearer token on API requests:
Authorization: Bearer <YOUR_API_KEY>Keep keys private
Medialyst only shows the raw API key once. Do not paste keys into prompts, commit them to source control, or share screenshots that reveal them.
2. Projects
Project endpoints require the projects:manage scope. Projects are available
to every organization; reading or changing memory and uploading or deleting
files follow the same plan rules as the app, so on the Free plan those calls
return 403 PROJECT_CONTEXT_UPGRADE_REQUIRED. The stable project id general
resolves to the Default project. You can read and rename it, but it cannot be
archived or deleted.
| Method | Endpoint | Purpose |
|---|---|---|
GET | /api/v1/projects | List active projects, counts, and timestamps |
POST | /api/v1/projects | Create a project from name and optional description |
GET | /api/v1/projects/{projectId} | Read one project and its file, media-list, and memory counts |
PATCH | /api/v1/projects/{projectId} | Update name and/or description |
DELETE | /api/v1/projects/{projectId} | Delete an empty project or archive one with history so its media lists remain reachable |
GET | /api/v1/projects/{projectId}/memory | Read Markdown memory, description, version, and update time |
PUT | /api/v1/projects/{projectId}/memory | Replace the complete memory document with compare-and-set protection |
POST | /api/v1/projects/{projectId}/memory/lines | Append one validated durable memory line |
GET | /api/v1/projects/{projectId}/radar | Read RADAR.md, recent opportunities, and redacted delivery status |
POST | /api/v1/projects/{projectId}/radar | Set up Radar, optionally with Slack delivery, and queue its first scan |
PUT | /api/v1/projects/{projectId}/radar | Replace RADAR.md with compare-and-set protection |
PATCH | /api/v1/projects/{projectId}/radar | Change schedule/delivery settings, configure or clear Slack, and optionally send a test |
POST | /api/v1/projects/{projectId}/radar/run | Queue an additional Radar scan |
GET | /api/v1/projects/{projectId}/files | List attached project files |
POST | /api/v1/projects/{projectId}/files | Start a three-step presigned binary upload |
POST | /api/v1/projects/{projectId}/files/{fileId}/complete | Verify the uploaded byte count and SHA-256 digest, then start extraction |
POST | /api/v1/projects/{projectId}/files:upload-text | Upload UTF-8 Markdown or plain text and start extraction in one call |
GET | /api/v1/projects/{projectId}/files/{fileId} | Poll extracting until the file is ready or failed |
DELETE | /api/v1/projects/{projectId}/files/{fileId} | Delete the file and stored object |
Memory compare-and-set
GET returns a numeric version. Send that value as expectedVersion with a
full-document PUT. If another writer saved first, the API returns 409 PROJECT_MEMORY_VERSION_CONFLICT; fetch the latest document, reapply the edit,
and retry. A PUT is a full replacement. For a single durable fact, prefer the
append endpoint so existing lines cannot be overwritten.
Project memory has four sections:
| Section | What belongs there | Line grammar |
|---|---|---|
About the client | Durable facts about the client, market, competitors, spokespeople, and tier language | One fact per line, with the pitching consequence when it has one |
Ranking preferences | Who ranks first/last, tiers, send order, and per-outlet A/B lists | <who or what> ranks <higher/lower/first/last> on <what>: <why> |
Never | No-exception exclusions | Never <action> <who>: <why> |
Pitch voice | Tone, sign-off rules, and words to avoid | One instruction per line |
Ranking preferences and Never lines without a reason after a colon are
rejected. Every appended line is 1–300 characters and keeps API-key provenance
in an inline Markdown comment.
File uploads
Use the three-step flow for PDF, DOCX, Markdown, or plain-text bytes:
POST /api/v1/projects/{projectId}/fileswithtitle,mimeType,byteSize, and an optional retry-safeclientUploadKey.PUTthe exact bytes to the returneduploadUrlusing the returned headers. This storage URL is not a Medialyst API endpoint.- Compute the lowercase SHA-256 digest and call
POST /api/v1/projects/{projectId}/files/{fileId}/completewithsha256.
For Markdown or plain text, the simpler
POST /api/v1/projects/{projectId}/files:upload-text accepts title, content,
optional mimeType (inferred from the title when possible, otherwise
text/markdown), and an optional
clientUploadKey. Medialyst computes UTF-8 bytes and SHA-256 server-side,
stores the object, and starts extraction. Reuse the same clientUploadKey when
you retry after a lost response: the API returns the file it already created
instead of a duplicate. The title may be a human-readable name without a known
project-file extension, including dotted titles such as Q3 results v2.1:
Medialyst appends .md by default or .txt for an explicit text/plain mime
type. When mimeType is omitted, a .txt title selects text/plain, while
.md and .markdown select text/markdown. A known project-file extension
that contradicts an explicit mime type is rejected. Both
flows accept at most 4 MB. Poll the file endpoint until ready; the ready
response includes its generated description.
Radar Slack delivery
Radar uses a Slack Incoming Webhook per project, not a Slack OAuth channel.
The webhook must be an HTTPS URL on hooks.slack.com under /services/.
Set it during Radar setup or later through the settings endpoint:
{
"slack_webhook_url": "https://hooks.slack.com/services/T000/B000/REDACTED",
"slack_digest_enabled": true,
"send_test": true
}On POST, supplying slack_webhook_url enables Slack by default unless
slack_digest_enabled is explicitly false. On PATCH, replacing the URL
preserves the current enabled state, so pass slack_digest_enabled: true when
enabling a previously disabled destination. send_test: true reuses Radar's
normal Slack test delivery and may be sent by itself to test the existing
enabled destination.
The webhook is a write-only secret. Responses and GET return only redacted
status under radar.delivery.slack, including enabled,
webhookConfigured, and webhookMasked; they never return the URL or its
encrypted database value. Set slack_webhook_url to null or "" to delete
the saved secret and disable Slack delivery. All Radar operations continue to
use projects:manage; existing OAuth grants do not need a new scope.
3. Poll Journalist Moves
GET /api/v1/journalist-moves returns de-duplicated journalist outlet-move
records observed after an explicit timestamp or opaque cursor. It requires an
active Scale-plan credential and costs zero credits. since filters on when
Medialyst observed each move — not the move's effective date — so a poller
still receives late-reported moves. Each move also carries a nullable
published_at (the source's own publication time) for consumers that want
recently-announced rather than recently-observed moves.
Follow the Journalist Moves API guide for the response schema, the exclusive UTC cursor semantics, the de-duplication rule, the match-state default, and a complete daily polling example.
4. Automated Media Lists
Follow the Automated Media Lists guide to choose between an approval-gated browser deep link and a fully automated API job. The guide covers creating a researched journalist list, polling its background job, opening the durable authenticated app URL, and retrieving either complete website-equivalent table data or the legacy normalized result projection.
The async create response now includes stable media_list_id and
workflow_id aliases, an absolute signed-in media_list_url, and an absolute
API-key-authenticated results_url as soon as the table exists. The table is
committed before the response, so integrations can post media_list_url
without a table-creation 404 race. The app URL resolves the table to its
organization's current slug, so a later slug rename does not break a link
already posted to a team. Medialyst does not create a public share or weaken
organization authorization for this link. A same-key replay of a legacy
table-less job can temporarily return null resource locators until its worker
attaches the table.
GET /api/v1/media-lists/{mediaListId}?include_rows=true&row_detail=full&limit=100
is the canonical complete-table follow-up. It returns ordered column identity
and type metadata plus full values keyed by column ID, source row data, and row
origin metadata. Paginate with page.next_cursor. The endpoint keeps the same
media_lists:manage scope and organization boundary as the signed-in website.
If an older list still contains contact values masked before that product gate
was removed, the read restores them for every plan. Existing clients can keep
using GET /api/v1/jobs/{jobId}?include=results for the smaller,
backward-compatible journalist_list_v1 projection.
target_list_size on POST /api/v1/media-lists:create-async is the most
credits the list may consume (1 to 1,000; the ceiling on paid plans), not a
row count. Each row that delivers a verified, deliverable email costs 1
credit; blank rows cost nothing and do not count against the budget. A
journalist with several matching articles appears once per article, and each
billable row costs a credit. The initial search is bounded by the target, then
the Beat Sweep keeps adding rows until the budget is spent or the angles run
dry, so result.total_rows can exceed target_list_size; the job's budget
object reports the cap, what has billed, and what rows still enriching hold.
Rule of thumb: ask for roughly twice the named journalists you want.
5. Enrich Journalists
POST /api/v1/journalists/enrich accepts known journalist references as well
as article URLs. The canonical reference request is:
{
"from": [
{
"type": "journalist_reference",
"name": "Jane Doe",
"publication": "The Daily Example"
}
]
}For every journalist_reference, name requires at least one of
publication, domain, or url. Use article_url with a real article URL
when you want to enrich that article's author; both source types can be mixed in
the same from array.
Enrichment is asynchronous. The create request returns a durable job id
immediately. Poll
GET /api/v1/journalist-enrichment-jobs/{jobId} until its status is complete
or failed. The deprecated create-call fields options.wait and
options.timeout_ms do not change this workflow.
This API uses outcome billing. It charges 1 credit for each source that returns a journalist with a verified, deliverable email and 0 credits for a source with no verified-email result. Misses are free — there is no upfront charge and no refund step.
6. Look Up Articles
GET /api/v1/articles/lookup returns canonical metadata — title, publish
date, author name and profile, site name, language, excerpt, and word count —
for one article URL. POST /api/v1/articles/lookup:batch does the same for
1-10 URLs. Add include=markdown to the single lookup when you need the
extracted body; it is the only accepted include value, and batch lookup does
not accept include at all. Author details are part of the default response.
Each successful URL costs 0.1 credit; per-URL failures cost 0 credits.
Follow the Article Lookup API guide for parameters, response shapes, error codes, and the lookup-specific rate limits.
7. Rate Limits
Limits are enforced per API key over a one-minute window unless noted. Most routes have no daily request cap: credits, not requests, are the consumable. Project-file creation is the exception because stored context is a bounded organization resource.
| Scope | Limit | Response when exceeded |
|---|---|---|
All /api/v1 routes | 60 requests per minute | 429 RATE_LIMITED, retry_after_seconds: 60 |
GET /api/v1/articles/lookup and POST /api/v1/articles/lookup:batch | 10 requests per minute (both routes counted together), plus 10 in-flight lookups per organization | 429 RATE_LIMITED, retry_after_seconds: 60 (per minute) or 1 (in flight) |
GET /api/v1/publications/lookup and POST /api/v1/publications/lookup:batch | 60 requests per minute; force_refresh lookups 5 per 15 minutes | 429 RATE_LIMITED, retry_after_seconds: 60 or 900 |
POST /api/v1/journalists/enrich | 10 active (pending or processing) jobs per key | 429 RATE_LIMITED, retry_after_seconds: 5 |
POST /api/v1/media-lists:create-async | 3 active (pending or processing) jobs per key; jobs complete on their own and deleting a list frees its slot | 429 RATE_LIMITED |
POST /api/v1/projects/{projectId}/files and POST /api/v1/projects/{projectId}/files:upload-text | 60 attempts per hour per key, plus the existing 60-upload daily safety cap for the creating user | 429 PROJECT_FILE_UPLOAD_RATE_LIMITED with Retry-After |
GET /api/v1/journalist-moves | 30 polls per minute | 429 RATE_LIMITED; see Journalist Moves errors |
Every 429 body has code: "RATE_LIMITED" and, where a wait is meaningful,
retry_after_seconds; the same value is sent as a Retry-After header.
Hosted MCP tools call these routes, so they share the same limits.
Handling 429
- Read
retry_after_secondsfrom the body (or theRetry-Afterheader), wait at least that long, then retry. Rejected requests still count toward the 60-per-minute window, so a tight retry loop only extends the wait. - Prefer batch routes for bursts: one
lookup:batchcall with 10 URLs is one request, while 10 single lookups use the whole article-lookup allowance. - For journalist enrichment and async media lists, a
429means the active-job cap, not request rate. Poll the existing jobs until they arecompleteorfailedbefore submitting more. - A daily limit applies only to project-file upload attempts. Other REST calls remain governed by minute, active-job, plan, and credit limits. The in-app chat has a separate daily message cap; see the FAQ.
8. API Documentation
Browse the interactive API documentation at /api/docs.
9. Agent-Readable OpenAPI Reference
Use the raw OpenAPI document at /api/open-api.