API and webhooks
Every account can run music outreach from its own tools. The API uses the same guarded paths as the interface, so the same entitlement, verification and sender checks apply. API access is available on the Pro Industry plan and above.
Base URL and authentication
https://spotifymonthlylisteners.com/api/v1Send your key as a bearer token on every request. Keys start with ask_live_.
Authorization: Bearer ask_live_…Create a key in your workspace under Developers. The plaintext is shown once and stored only as a SHA-256 hash, so a lost key cannot be recovered — create a new one and revoke the old. Each key is scoped to a single account; it can never read another account's lists, campaigns or replies.
Endpoints
| Method | Path | Body | Returns |
|---|---|---|---|
| GET | /leads | — | Your lead lists and their stored contact counts. This is not a recipient export. |
| POST | /leads | {"name":"…","text":"email,name lines"} | Parses, verifies and imports contacts. Returns the stored list id and how many were added. |
| GET | /templates | — | Your saved email templates: id, name and subject. |
| POST | /templates | {"name":"…","subject":"…","body":"…"} | Creates a template. Plain text is converted to simple HTML paragraphs. |
| GET | /campaigns | — | Your campaigns with running state, counts and list bindings. |
| POST | /campaigns | {"name":"…","listIds":["…"],"templateId":"…","replyTo":"…","activate":false} | Creates one campaign per list through the normal send gates. `activate` defaults to false. |
| GET | /stats | — | Aggregated totals across your own campaigns: campaigns, sent, opens and replies. |
| GET | /replies | — | Your most recent persisted replies. |
| GET | /verify | — | Verification usage and dashboard for the current month. |
| POST | /verify | {"emails":["a@b.com"]} or {"text":"…"} | Runs recipient verification and returns the per-address results and held addresses. |
Examples
Set the key in your own environment; never commit it.
export PORTAL_CLIENT_API_KEY="ask_live_…"
# List your lead lists
curl --fail-with-body --silent --show-error \
-H "Authorization: Bearer $PORTAL_CLIENT_API_KEY" \
https://spotifymonthlylisteners.com/api/v1/leads
# Check verification usage
curl --fail-with-body --silent --show-error \
-H "Authorization: Bearer $PORTAL_CLIENT_API_KEY" \
https://spotifymonthlylisteners.com/api/v1/verifyCreate a campaign. Save JSON to a file so newlines survive the shell.
cat > campaign.json <<'JSON'
{
"name": "Single release outreach",
"listIds": ["<owned-list-id>"],
"templateId": "<owned-template-id>",
"replyTo": "you@your-domain.com",
"activate": false
}
JSON
curl --fail-with-body --silent --show-error \
-X POST -H "Authorization: Bearer $PORTAL_CLIENT_API_KEY" \
-H "Content-Type: application/json" \
--data @campaign.json \
https://spotifymonthlylisteners.com/api/v1/campaignsEvery response is { "ok": true, "data": … } on success and{ "ok": false, "error": "CODE" } on failure. Verify the stored ids and counts in the response rather than assuming the write landed.
Error codes
| Code | HTTP | Meaning |
|---|---|---|
| INVALID_API_KEY | 401 | The key is missing, revoked or does not belong to an active account. |
| VERIFICATION_PAID_REQUIRED | 402 | The account is on a trial. Verification, lead import and sending require an activated paid plan or an admin grant. |
| NOT_FOUND | 404 | That resource path is not implemented, or the requested object is not yours. |
| INVALID_INPUT | 400 | A required field is missing or malformed. |
| INVALID_NAME | 400 | A list or template name must be between 2 and 80 characters. |
| NO_LEADS_FOUND | 400 | No usable email addresses were found in the submitted text. |
| CAMPAIGN_CREATE_FAILED | 400 | Campaign creation was rejected. The response carries a more specific code when one is known. |
| VERIFICATION_REQUIRED | 409 | The list must be verified before it can be sent to. |
| VERIFICATION_HELD_RECIPIENTS | 409 | The list still contains invalid or unresolved addresses. Remove them and verify again. |
| VERIFICATION_LIMIT_REACHED | 409 | The plan verification allowance for this month is used up. |
| VERIFICATION_RATE_LIMIT | 409 | Too many verification requests in a short period. |
| LIST_IS_SENDING | 409 | The list is in use by a running campaign. |
| LIST_CHANGED_RETRY | 409 | The list changed while it was being read. Read it again and retry. |
| LAUNCH_IN_PROGRESS | 409 | Another launch for this account is already in progress. |
| PROVIDER_UNAVAILABLE | 502 | The email provider could not be read. This is a transient failure — it never means your data is empty. |
| WORKSPACE_UNAVAILABLE | 503 | The sending workspace is not available for this account. |
| VERIFICATION_UNAVAILABLE | 503 | Verification could not run. Retry; unknown addresses stay held. |
Signed webhooks
Register an HTTPS endpoint in your workspace and choose which events you want. Each delivery is a POST with a JSON body and an HMAC-SHA256 signature over the raw request body.
| Event | Fires when |
|---|---|
| campaign.created | A campaign was created and is saved. |
| campaign.launched | A campaign was activated and starts sending. |
| campaign.paused | A campaign was paused or stopped. |
| leads.imported | A lead list finished importing with verified contacts. |
| reply.received | A reply was read from a connected mailbox. |
Headers
Content-Type: application/json
X-Webhook-Event: campaign.launched
X-Webhook-Signature: sha256=<hex HMAC of the raw body, keyed with your signing secret>Verify a delivery
Sign the raw body bytes before any JSON parsing or re-serialisation, and compare in constant time.
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyPortalWebhook(rawBody, signature, secret) {
if (!secret || typeof signature !== 'string') return false;
const expected = Buffer.from(
'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex'),
'utf8',
);
const supplied = Buffer.from(signature, 'utf8');
return supplied.length === expected.length && timingSafeEqual(supplied, expected);
}Limits you should design for
- • Webhook delivery has an eight-second timeout and is best-effort. Failures are recorded against the endpoint but not retried, and there is no delivery id — deduplicate on your side and reconcile counts periodically with
/statsand/campaigns. - • A signature proves the payload came from us; it does not prevent replay. Use your own freshness check or idempotency key.
- • Reads are cached and paced against the email provider. Repeated polling of the same resource will not return faster; back off on a 429.
- • Verification is limited to 5,000 addresses per request and by your monthly allowance. Split larger batches.
- • Creating a campaign can take up to a minute because of provider pacing; a slow response is not a failure, and a timeout can follow a partial write — read the campaign back before retrying.
- • Sending volume, inbox placement and curator replies are never guaranteed by the API or by the interface.
Honest scope of this API
The API gives you direct access to your own campaign data and the music contact library. It does not sell or guarantee playlist placements, radio spins, press coverage, streams or audience growth, and it cannot bypass entitlement or verification checks. Respect the recipients you contact and any applicable anti-spam law in their country.