Developers

Automated Media Lists

Medialyst can turn a campaign brief into a researched journalist list in three ways: hand the brief to a person with a browser deep link, call the asynchronous REST API, or run the same asynchronous contract through the hosted MCP server.

All three methods create the same organization-scoped media-list table used by the Medialyst app.

Choose A Method

MethodBest forHuman approvalWhen list creation starts
Browser deep linkButtons, CRM links, internal tools, and handoffs to a personRequiredAfter the person reviews and approves the plan
Asynchronous REST APIAgents, backend services, scheduled jobs, and unattended workflowsNot requiredImmediately after the API accepts the request
Hosted MCPMCP-capable agents that should create and inspect a list in one tool sessionNot requiredImmediately after the create tool is accepted

Use the API as the primary method when the workflow should complete without a person: choose hosted MCP for an MCP-capable agent and REST for a backend service. Use a deep link when you want Medialyst to prepare the workflow but keep a person in control of approval and credit spend.

Fully Automated MCP

Connect to https://medialyst.ai/api/mcp, then use this exact tool sequence:

  1. create_media_list with prompt, the credit-budget target_list_size, and a stable request_options.idempotency_key.
  2. get_media_list_job with the returned job_id until status is complete, failed, or cancelled.
  3. On completion, get_media_list with the returned media_list_id, include_rows: true, row_detail: "full", and limit: 100; continue with page.next_cursor until it is null.

The create response exposes job_id, media_list_id, workflow_id, media_list_url, and results_url, and each MCP response includes next_tool guidance. These tools delegate to the REST operations documented below, so authentication, scopes, organization isolation, plan limits, free-tier behavior, budget settlement, idempotency, errors, and response fields do not form a separate MCP contract. See Medialyst MCP for connection instructions.

Send a signed-in user to this URL with a URL-encoded campaign brief:

https://medialyst.ai/app/_/workflow/campaign?prompt=[URL-ENCODED-PROMPT]

The _ segment resolves to the user's current organization. Medialyst preserves the destination through sign-in or onboarding, automatically submits the prompt to the campaign agent, and presents the proposed plan for review. No credits are spent until the user approves the plan. Normal media-list credit usage begins after approval.

Build the link with a URL API instead of concatenating unescaped text:

const prompt =
    "Find Canadian journalists covering PR technology and AI media tools.";
const url = new URL("https://medialyst.ai/app/_/workflow/campaign");

url.searchParams.set("prompt", prompt);

console.log(url.toString());

The resulting link can be placed behind a Build in Medialyst button, returned by another agent, or added to a CRM record. Because query strings can appear in browser history and server logs, do not put credentials or other secrets in the prompt.

Fully Automated REST API

Use the asynchronous media-list API when an agent or backend service should create and retrieve the list without waiting for a person to approve it. The create request returns immediately with a job ID, stable list/table identifiers, and a durable Medialyst URL while discovery and enrichment run in the background.

The API bypasses the browser approval step and starts list creation as soon as the request is accepted. Normal media-list credits and plan limits apply.

An API list gets the same treatment as a list approved in the app: after the initial search and Journalist Profile enrichment, a Beat Sweep researches every angle in the plan and keeps adding the journalists it finds until your credit budget is spent or the angles run dry. See How a list is built below.

The API creates the empty table before returning 202 Accepted, so the returned media_list_url does not have a table-creation 404 race. The list appears on the Media Lists page immediately for members of the API key's organization. If that page is already open, refresh it to load lists created outside the browser session.

Before You Start

Create an API key from the Medialyst Developers page with the media_lists:manage scope. Store the key in secret storage or an environment variable such as MEDIALYST_API_KEY.

All examples below use https://medialyst.ai/api as the API base URL.

1. Create A List Job

Send a campaign brief and the most credits the list may use:

curl --request POST \
  'https://medialyst.ai/api/v1/media-lists:create-async' \
  --header "Authorization: Bearer $MEDIALYST_API_KEY" \
  --header 'Content-Type: application/json' \
  --header 'Idempotency-Key: launch-brief-2026-07' \
  --data '{
    "prompt": "Find US journalists covering enterprise AI infrastructure and cloud spending for our product launch.",
    "target_list_size": 50
  }'
FieldTypeRequiredDescription
promptstringYesCampaign idea, press release, or targeting brief. Maximum 2,000 characters.
target_list_sizeintegerYesThe maximum credits the list may consume, from 1 to 1,000 (the ceiling on paid plans; lower plan limits apply). One credit buys one row that delivers a verified, deliverable email. The finished list can hold more rows than this number.

The response is accepted immediately:

{
    "job_id": "job_01HT...",
    "status": "pending",
    "target_list_size": 50,
    "status_url": "/api/v1/jobs/job_01HT...",
    "media_list_id": "tbl_01HS...",
    "workflow_id": "tbl_01HS...",
    "media_list_url": "https://medialyst.ai/app/media-lists/tbl_01HS...",
    "results_url": "https://medialyst.ai/api/v1/media-lists/tbl_01HS...?include_rows=true&row_detail=full&limit=100"
}

Use the returned target_list_size as the effective budget. It can be lower than the requested value when the organization's plan has a lower limit.

media_list_id and workflow_id are two names for the same stable table identifier. media_list_url is an absolute, signed-in Medialyst app URL: a Teams bot can post it immediately, and Medialyst still checks the visitor's session and organization membership before redirecting to the table under the organization's current slug. It survives organization slug changes. It is not a public share link and does not contain a share token. results_url is the canonical API-key-authenticated full-table read described in Read Results.

Target size is a credit budget, not a journalist count

target_list_size is the most credits the list may consume. Each row that delivers a verified, deliverable email costs one credit; rows that never resolve to a journalist are left blank, are not backfilled, and cost nothing, so they do not count against the budget. A journalist with several matching articles appears once per article, and each of those rows that delivers an email costs a credit. Rule of thumb: ask for roughly twice the number of named journalists you want.

How A List Is Built

  1. Planning. The prompt is turned into a story idea, search keywords, and up to five named angles.
  2. Initial search. Articles are collected per angle, up to target_list_size rows, and the table is created with one row per article. Rows show Via API in the pinned Sources column.
  3. Enrichment. Every row runs Journalist Profile enrichment (byline, outlet, verified email, fit score). Each row reserves one credit from the budget while it enriches; a row that delivers a verified, deliverable email keeps its credit as spend, and any other outcome releases it.
  4. Beat Sweep. One Beat Sweep Agent per angle researches the beat and adds further article rows, tagged with the angle name, until the budget is spent or the angles run dry. Because blank rows give their credit back, the sweep keeps going past target_list_size rows when it has to, so result.total_rows can exceed target_list_size. The sweep runs alongside enrichment and never bills past the budget. It is skipped when the plan has no angles or when the organization has no credits left; the job's beat_sweep object says which.

A list stops growing at twice target_list_size rows, whatever the budget says, and the sweep also ends on its own time and search limits. A job with an unspent budget at that point has simply run out of things to find.

Billing

Each row that delivers a verified, deliverable email address costs one credit, whether it came from the initial search or from the Beat Sweep, and the list never bills more than target_list_size credits in total. Blank rows and rows without a verified email 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. See the credits guide for balances and top-ups.

Idempotency

Send a stable, unique Idempotency-Key for every logical creation request. Keys are scoped to your account and never expire. Repeating the same key with the same prompt and effective target returns the existing job, whatever its current state, as a 202 Accepted with the same job_id and its current status. Reusing a key with different input returns 409 Conflict. Use a new key to create a genuinely new list.

During rollout, replaying a job created before durable table preallocation can temporarily return null for media_list_id, workflow_id, media_list_url, and results_url. Poll status_url; the legacy worker fills those fields after it attaches the table. Newly created jobs always return the locators shown above.

For compatibility, existing integrations may continue sending topic instead of prompt and max_articles instead of target_list_size. New integrations should use the canonical fields shown above. If both size fields are supplied, their values must match.

2. Poll The Job

Poll the returned status URL every 10 to 15 seconds until status is complete, failed, or cancelled. Jobs complete on their own: the worker marks a job complete once every Journalist Profile row has finished and the Beat Sweep (if one ran) has ended, whether or not anyone polls. There are no webhooks yet.

curl \
  'https://medialyst.ai/api/v1/jobs/job_01HT...' \
  --header "Authorization: Bearer $MEDIALYST_API_KEY"

While the list is being built, the response includes the coarse stage, progress, row counts, the Beat Sweep state, and the credit budget:

{
    "job_id": "job_01HT...",
    "status": "processing",
    "stage": "beat_sweep",
    "target_list_size": 50,
    "media_list_id": "tbl_01HS...",
    "workflow_id": "tbl_01HS...",
    "media_list_url": "https://medialyst.ai/app/media-lists/tbl_01HS...",
    "results_url": "https://medialyst.ai/api/v1/media-lists/tbl_01HS...?include_rows=true&row_detail=full&limit=100",
    "progress": {
        "stage": "beat_sweep",
        "percent": 72,
        "message": "Beat Sweep researching angles"
    },
    "result": {
        "workflow_id": "tbl_01HS...",
        "total_rows": 57,
        "ready_rows": 41,
        "failed_rows": 2
    },
    "beat_sweep": {
        "status": "dispatched",
        "rows_added": 21,
        "run_id": "run_01HV..."
    },
    "budget": {
        "credits": 50,
        "spent": 31,
        "reserved": 14
    }
}

Once the job is complete, reserved is 0 and spent is what the list billed:

{
    "job_id": "job_01HT...",
    "status": "complete",
    "stage": "done",
    "target_list_size": 50,
    "media_list_id": "tbl_01HS...",
    "workflow_id": "tbl_01HS...",
    "media_list_url": "https://medialyst.ai/app/media-lists/tbl_01HS...",
    "results_url": "https://medialyst.ai/api/v1/media-lists/tbl_01HS...?include_rows=true&row_detail=full&limit=100",
    "progress": {
        "stage": "complete",
        "percent": 100,
        "message": "Job completed (2 rows could not be enriched)"
    },
    "result": {
        "workflow_id": "tbl_01HS...",
        "total_rows": 63,
        "ready_rows": 61,
        "failed_rows": 2
    },
    "beat_sweep": {
        "status": "completed",
        "rows_added": 27,
        "run_id": "run_01HV..."
    },
    "budget": {
        "credits": 50,
        "spent": 48,
        "reserved": 0
    }
}

Status values are:

StatusMeaning
pendingThe request was accepted but has not started.
processingPlanning, journalist enrichment, or the Beat Sweep is running.
completeEvery Journalist Profile row has finished (completed or failed) and the Beat Sweep, if one ran, has ended.
failedThe job stopped. Inspect the response's error object and retry only when retryable is true.
cancelledThe media list was deleted while the job was open (error.code is TABLE_DELETED). Deleting a list frees its slot.

stage is one of planning, enrichment, beat_sweep, done, failed, or cancelled. result.ready_rows counts rows whose Journalist Profile completed; result.failed_rows counts rows whose enrichment failed permanently. A job can be complete with ready_rows below total_rows.

beat_sweep is null until the sweep is dispatched, then carries a status of dispatched, completed, cancelled, failed, or skipped (with a reason such as no_angles, target_reached, or out_of_credits), plus rows_added, the number of rows the sweep put in the table.

budget is null during planning until the table's first rows and credit budget are installed, then reports target_list_size as a credit budget: credits is the cap, spent is what rows with a verified email have billed so far, and reserved is what rows still enriching are holding. The table identifiers and URLs are already present while budget is null. spent never exceeds credits. Compare result.total_rows with budget.spent to see how many rows came back blank.

The API allows at most three active (pending or processing) media-list jobs per API key. Jobs leave that count when they complete, fail, or are cancelled, and deleting a list cancels its job.

3. Read Results

For the complete first-party table used by the Medialyst website, call the returned results_url with the same bearer key:

curl \
  'https://medialyst.ai/api/v1/media-lists/tbl_01HS...?include_rows=true&row_detail=full&limit=100' \
  --header "Authorization: Bearer $MEDIALYST_API_KEY"

The response includes table metadata, ordered columns (id, display name, and workflowType), and full row values keyed by those column IDs. It also carries the source data and safe row-origin metadata used by the website's pinned Sources column. This preserves fields beyond the normalized journalist projection, including the source Article, full Journalist Profile, Score, Pitch Angle, Why They Fit, and any columns authorized users add while refining the table. Follow page.next_cursor by sending it as cursor until it is null; each page can contain up to 100 rows.

The endpoint uses the same media_lists:manage scope and organization boundary as the signed-in table. Contact masking and per-contact unlocks are no longer product gates; if an older list still contains masked contact values, the read restores them for every plan. The URL is not public: callers must keep sending the API key. Leave include_schema unset (its default is false) to receive column identity/type metadata without workflow execution configuration.

When Medialyst temporarily pauses new free-tier search work during a capacity incident, existing job polling and results_url reads remain available.

The original normalized projection remains available for existing integrations. Add include=results to the job endpoint:

curl \
  'https://medialyst.ai/api/v1/jobs/job_01HT...?include=results&limit=50' \
  --header "Authorization: Bearer $MEDIALYST_API_KEY"

The response returns normalized journalists in the top-level rows array. Each row uses the backward-compatible journalist_list_v1 shape, including journalist identity and contact fields, outlet information, match score and reasoning, recent articles, and the source workflow ID. Use page.next_cursor as cursor on the next request until it is null. A page can contain at most 200 rows. New integrations that need website-equivalent columns should use results_url; include=results intentionally remains a smaller stable projection for legacy clients.

Results can be requested while the job is processing. Fields produced by Journalist Profile enrichment may remain null until the corresponding row is ready.

When an address belongs to a contributor's own current organisation rather than the listed outlet, signals.email_source is "affiliation" and journalist.email_affiliation contains its organization and domain. These labelled, deliverable addresses are normal enriched-email results and are chargeable under the same rules as other returned email addresses.

UI Visibility And Ownership

  • The generated list is visible to members of the same organization as the API key.
  • It counts toward that organization's active media-list allowance.
  • The table exists before 202 Accepted is returned, so authorized users can open media_list_url immediately while its name, columns, and rows are filled in.
  • A pipeline or dispatch failure leaves the durable empty or partially populated list available for inspection; the job's error explains the failure.
  • No public share is created automatically. A signed-in visitor must belong to the API key's organization, and API reads still require the bearer key.

Continue Building The List

The deep link or API creates the starting table; it is not a one-time export. Open the finished media list and use Add journalists to append people by name, from a CSV, from article URLs, through news search, or with AI discovery.

The pinned Sources column records each batch and when it was added. API-created rows show Via API; later rows keep the label for their own addition path. See Find Journalists for the five paths and Tables for source filtering and deduplication.

Common Errors

HTTP statusMeaningWhat to do
400Missing, empty, mismatched, or out-of-range inputCorrect the request body before retrying.
401Missing or invalid API keyCheck the bearer token.
403Key lacks media_lists:manageCreate or use a key with the required scope.
402Organization cannot create another listDelete an existing list or change the plan before retrying.
409Idempotency key was reused with different inputUse the original input or a new idempotency key.
429Request rate or active-job limit reachedBack off before retrying; wait for active jobs to finish.

Each API key can have at most 3 list jobs in pending or processing at once; a fourth create call returns 429 RATE_LIMITED until one finishes. Jobs finish on their own without polling, and deleting a list frees its slot immediately. The API-wide limit is 60 requests per minute per key — see Rate Limits.

For the generated request schema and operation metadata, see the interactive REST API reference or raw OpenAPI document.