Developers

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.

MethodEndpointPurpose
GET/api/v1/projectsList active projects, counts, and timestamps
POST/api/v1/projectsCreate 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}/memoryRead Markdown memory, description, version, and update time
PUT/api/v1/projects/{projectId}/memoryReplace the complete memory document with compare-and-set protection
POST/api/v1/projects/{projectId}/memory/linesAppend one validated durable memory line
GET/api/v1/projects/{projectId}/radarRead RADAR.md, recent opportunities, and redacted delivery status
POST/api/v1/projects/{projectId}/radarSet up Radar, optionally with Slack delivery, and queue its first scan
PUT/api/v1/projects/{projectId}/radarReplace RADAR.md with compare-and-set protection
PATCH/api/v1/projects/{projectId}/radarChange schedule/delivery settings, configure or clear Slack, and optionally send a test
POST/api/v1/projects/{projectId}/radar/runQueue an additional Radar scan
GET/api/v1/projects/{projectId}/filesList attached project files
POST/api/v1/projects/{projectId}/filesStart a three-step presigned binary upload
POST/api/v1/projects/{projectId}/files/{fileId}/completeVerify the uploaded byte count and SHA-256 digest, then start extraction
POST/api/v1/projects/{projectId}/files:upload-textUpload 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:

SectionWhat belongs thereLine grammar
About the clientDurable facts about the client, market, competitors, spokespeople, and tier languageOne fact per line, with the pitching consequence when it has one
Ranking preferencesWho ranks first/last, tiers, send order, and per-outlet A/B lists<who or what> ranks <higher/lower/first/last> on <what>: <why>
NeverNo-exception exclusionsNever <action> <who>: <why>
Pitch voiceTone, sign-off rules, and words to avoidOne 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:

  1. POST /api/v1/projects/{projectId}/files with title, mimeType, byteSize, and an optional retry-safe clientUploadKey.
  2. PUT the exact bytes to the returned uploadUrl using the returned headers. This storage URL is not a Medialyst API endpoint.
  3. Compute the lowercase SHA-256 digest and call POST /api/v1/projects/{projectId}/files/{fileId}/complete with sha256.

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.

ScopeLimitResponse when exceeded
All /api/v1 routes60 requests per minute429 RATE_LIMITED, retry_after_seconds: 60
GET /api/v1/articles/lookup and POST /api/v1/articles/lookup:batch10 requests per minute (both routes counted together), plus 10 in-flight lookups per organization429 RATE_LIMITED, retry_after_seconds: 60 (per minute) or 1 (in flight)
GET /api/v1/publications/lookup and POST /api/v1/publications/lookup:batch60 requests per minute; force_refresh lookups 5 per 15 minutes429 RATE_LIMITED, retry_after_seconds: 60 or 900
POST /api/v1/journalists/enrich10 active (pending or processing) jobs per key429 RATE_LIMITED, retry_after_seconds: 5
POST /api/v1/media-lists:create-async3 active (pending or processing) jobs per key; jobs complete on their own and deleting a list frees its slot429 RATE_LIMITED
POST /api/v1/projects/{projectId}/files and POST /api/v1/projects/{projectId}/files:upload-text60 attempts per hour per key, plus the existing 60-upload daily safety cap for the creating user429 PROJECT_FILE_UPLOAD_RATE_LIMITED with Retry-After
GET /api/v1/journalist-moves30 polls per minute429 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_seconds from the body (or the Retry-After header), 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:batch call with 10 URLs is one request, while 10 single lookups use the whole article-lookup allowance.
  • For journalist enrichment and async media lists, a 429 means the active-job cap, not request rate. Poll the existing jobs until they are complete or failed before 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.