Rate limits and HTTP 429
The ESP API does not publish a guaranteed per-second rate limit. Treat HTTP 429 (Too Many Requests) as the authoritative signal: if you receive it, slow down or add caching in your client.
Practical guidance:
-
Start small. Begin with the minimum throughput your use case needs and scale up while watching for 429s. Multiple requests per second is generally acceptable for endpoints like
/public/widget/data, but exact ceilings depend on server load and the size of your account. -
Back off and cache. When you see 429s, decrease the request rate and cache responses where the data does not need to be real-time.
-
Coordinate large batches with Piano. Large extractions (for example, hundreds of thousands of records in one job) are governed by contract page-view volume as well as infrastructure capacity. Confirm scope with your Piano contact before running them.
For reference, the rate limits on other Piano APIs are higher and publicly stated: Identity Management and Management + Billing allow up to 200 calls per second, with Management + Billing additionally capped at 50,000 calls per 5 minutes. These do not apply to ESP - they are listed here only so you do not assume the same ceilings hold on ESP endpoints.
For ESP webhook handlers, rate-limit interactions on the receiving side can also cause problems: if your endpoint is throttled by the downstream ESP, webhook events may not be processed and subscription state can drift. If you suspect this, work with the receiving system's support to raise the limit, temporarily or permanently.
CPAKM: Bad Request (0) - wrong API key
A CPAKM: Bad Request (0) response when calling ESP endpoints (commonly when listing mailing lists) almost always means the wrong API key is being used. The ESP API requires its own ESP-specific API key, not the general Piano API Token used for Composer, Management + Billing, or Identity Management.
To find the correct key:
-
Open the ESP dashboard.
-
Go to Setup > Integrations.
-
The API key is at the top right of the page. Some UIs require clicking the (i) information button to reveal it.
In sandbox the path is the same but the URL prefix changes: https://sandbox-esp.piano.io/#/setup/integrations?siteId=.
If no key is visible there, contact Piano Support to have one created for you.
mlids formatting errors
The mlids parameter on /tracker/securesub must be formatted differently depending on the request content type:
-
application/x-www-form-urlencoded- sendmlidsas a single string. For one ID, that is the ID itself ("5628"); for multiple, a comma-separated string ("5628,5629"). -
application/json- sendmlidsas a JSON array ([5628, 5629]).
The common failure mode is an HTTP client (Guzzle, for example) that serializes a PHP array into form parameters as mlids[0]=5628&mlids[1]=5629. The API does not accept that shape and returns a bad-request error.
// PHP / Guzzle - form-encoded with a flat comma-separated string
$response = $this->client->request($method, $uri, [
'form_params' => [
'email' => $email,
'mlids' => implode(',', $mlidArray), // "5628" or "5628,5629"
],
]);
// PHP / Guzzle - JSON body with an array
$response = $this->client->request($method, $uri, [
'headers' => ['Content-Type' => 'application/json'],
'json' => [
'email' => $email,
'mlids' => [5628],
],
]);
Also verify that the mailing list IDs exist in the environment you are calling against. IDs from the production dashboard will not work against sandbox, and vice versa.
401 Unauthorized and 403 Access Denied
These two errors mean different things:
-
401 Unauthorized. The request reached the right endpoint but the credentials or required body parameters are wrong. On the list-active-subscribers endpoint (
POST /publisher/pub/{ESP_ID}/sq/subscribers), the most common cause is omittingsqIdsfrom the body. Otherwise the API key is invalid for this site. -
403 Access Denied. Either the API URL is wrong (for example, hitting
api-esp.piano.iofor an EU account that should hitapi-esp-eu.piano.io) or the account associated with the API key does not have permission for the operation. Check the region and confirm with an admin that the user holding the key has the right permissions.
If access issues persist after both checks, confirm the key has not been rotated and is not being shared across systems.
Cloudflare blocks on subscription calls
If you call the ESP API from a webpage (for example, a JavaScript subscription form) and the request fails with a Cloudflare security error, the request is being flagged as suspicious before it reaches Piano. This usually happens when the call does not look like ordinary browser traffic.
Things to try:
-
Lower the Cloudflare Security Level. In Cloudflare, go to Firewall > Settings and reduce the Security Level so legitimate API calls are not challenged. Verify that the rule covering your origin allows the ESP endpoint.
-
Reproduce in a REST client. Insomnia or Postman often succeed where a browser-originated
fetchis blocked, because Cloudflare's heuristics treat them differently. If the REST client succeeds, the problem is on the Cloudflare side, not on Piano's. -
Whitelist the Piano endpoints. If your Cloudflare config supports allow-listing specific destinations or user agents, allow-list the ESP API host.
ESP blacklist on email addresses
When you add a user via the API and the system immediately reports them as unsubscribed with a message like "ESP blacklist," the email address has been judged untrusted by Piano's deliverability protections.
The blacklist exists to protect your sender reputation. The most common reason an address ends up blacklisted is that it belongs to a disposable / temporary email service (Mailinator and similar). These addresses are commonly used to spoof signups; including them in mailings hurts deliverability across your entire program.
There is no API-level override for the blacklist - the protection is intentional. If you believe a legitimate address has been flagged, contact Piano Support with the specific email and the context of how it was collected.
Endpoint allow-listing and X-Frame-Options
If newsletter content fails to load - for example, images do not appear, or embedded ESP elements display blank - your security headers may be blocking the ESP API host.
Specifically, the X-Frame-Options header should permit the relevant ESP base URL:
-
api-esp.piano.io(Americas) -
api-esp-eu.piano.io(Europe) -
api-esp-ap.piano.io(Asia-Pacific) -
sandbox-api-esp.piano.io(sandbox)
If you have a stricter Content Security Policy or X-Frame-Options configuration, add the matching ESP host to it. Verify with Piano Support whether any additional endpoints need to be allow-listed for your specific use case.
Users do not receive newsletters / silent unsubscribe
If a user is in your dashboard as subscribed but is not receiving sends, query their subscription status directly:
GET https://api-esp.piano.io/tracker/securesub/email/<email>/ml/<mailing_list_id>?api_key=<api_key>
The HTTP response code and reason indicate what happened:
|
HTTP code |
Reason |
Meaning |
|---|---|---|
|
200 |
- |
Subscribed. |
|
(no reason) |
Never subscribed. |
|
|
subscription status |
Deactivated (via API, dashboard, or email link). |
|
|
subscription status |
Hard bounce - invalid email. |
|
|
subscription status |
Marked the email as spam. |
|
|
subscription status |
Email blacklisted by ESP. |
|
|
500 |
- |
Server error - check logs or contact support. |
If the result is 0 (deactivated) and you do not know why, the deactivation often originated from an external API call - a batch process, an integration handler, or a downstream ESP that re-pushed an unsubscribe. See the "Batch processes conflicting with API integrations" section below for the diagnostic flow.
To re-subscribe a user, call POST /tracker/securesub with the correct mailing list and campaign IDs. If the resubscribe is immediately reversed by another API call, that is your sign to look for the conflicting process. In stubborn cases, escalate to support so they can re-subscribe the user manually.
Newsletters deactivate automatically (webhook safeguard)
If a recurring newsletter shows as "Disabled" without anyone toggling it off, the cause is almost always the webhook safeguard.
How it works: a webhook endpoint is marked Inactive if it fails to return HTTP 200 for roughly 7 consecutive days, or after 237 failed retries. When the webhook that powers a recurring campaign is deactivated, the corresponding newsletter in the ESP is also marked Disabled so the campaign does not keep trying to send through a broken pipe.
There is no separate ESP timer that disables a newsletter on its own. If a newsletter is disabled, the only causes are (a) the webhook auto-deactivation above, or (b) an internal user manually toggling it off.
Diagnose and resolve
-
Check webhook status. In the dashboard, go to Manage > Webhooks. Look for endpoints showing "Inactive." Expand the row to see error details (4xx/5xx responses, timeouts, TLS errors).
-
Check newsletter status. In the Email Manager, confirm the affected newsletters are marked Active.
-
Confirm notification email. In Business Settings, ensure the "Notification email" is correct. Piano sends early-warning notifications to this address with subject codes such as
TO_PUBLISHER_WEBHOOK_UNSUCCESSFUL_ATTEMPTS,TO_PUBLISHER_WEBHOOK_ENDPOINT_DEACTIVATING_SOON, andTO_PUBLISHER_WEBHOOK_DEACTIVATED.
Reactivate and prevent recurrence
-
Fix the endpoint. The endpoint must return HTTP 200 within 30 seconds. Use cURL or Postman to verify, then resolve any TLS, firewall, or routing issues that surface.
-
Reactivate the webhook via Manage > Webhooks. Note that events missed while the webhook was down cannot be replayed.
-
Re-enable affected newsletters in the Email Manager.
-
Add uptime monitoring on the endpoint so you get alerted as soon as it stops returning 200. Optionally, poll
/publisher/webhook/liston a schedule to maintain an audit trail of webhook status changes.
As long as the endpoint consistently returns HTTP 200, webhooks will remain active and newsletters will stay enabled unless the 7-day / 237-retry threshold is tripped.
Batch processes conflicting with API integrations
If users unsubscribe, you confirm the unsubscribe, and then they continue to receive emails (or vice versa), the most likely cause is a batch process that re-pushes subscription state from an external source of truth.
The classic shape of the problem: you have two pools of email addresses - those that exist only in the ESP, and those that exist in both the ESP and an internal database (CRM, data warehouse, etc.). A scheduled job synchronizes the second pool from the internal database to the ESP. If a user unsubscribed in the ESP but is still marked as "subscribed" in the internal database, the next sync will re-subscribe them.
How to diagnose
-
Look at the timing. Compare the timestamps of unexpected subscription changes against your scheduled jobs. Re-subscriptions happening on a fixed cadence (every night, every Sunday) are a strong signal.
-
Check the logs. API execution logs - whether on Piano's side via support, or on your side in the integration code - record every state change. Identify which process made the offending call.
-
Audit manual / spreadsheet processes. Manual subscription edits made through CSV upload or operator action can also be overridden by automated batch jobs that run afterwards.
-
Engage development teams. Once the conflicting process is identified, the fix is almost always in the source-side logic: stop re-asserting subscribed state for users who unsubscribed in the ESP. Common approaches are to (a) treat ESP as authoritative for subscription state and pull from it, or (b) record unsubscribes in the internal database and skip them on outbound sync.
No API endpoint for exporting subscribers
The ESP module API does not support a full export of email or subscriber lists. There is no endpoint that returns every subscriber on a mailing list.
What is available:
-
"Download mailing list" button in the ESP dashboard - manual full export.
-
/publisher/pub//ml//sqand/publisher/pub//sq- list mailing-list metadata (IDs, names, status, campaign associations). No per-subscriber data. -
Mine Users export (
/publisher/export/create/userExport+/publisher/export/get+/publisher/export/download) - asynchronous export of users matching criteria you define. This is the closest thing to an automated export, and it is the right tool when you need to extract subscriber data programmatically.
If you genuinely need per-mailing-list full exports automated, raise the requirement with your Piano contact - the existing endpoints do not solve it.