REST API v1

Base URL: https://mailsaas.searchpilot.store/api/v1

Authentication

Create a key in Workspace → API Keys (plans with API access). Send it as a bearer token. Keys are scoped (contacts:read, contacts:write, campaigns:read, campaigns:write, automations:read, automations:write, account:read) and belong to one workspace.

curl https://mailsaas.searchpilot.store/api/v1/contacts \ -H "Authorization: Bearer ms_live_xxxxxxxxxxxxxxxx" \ -H "Accept: application/json"

Rate limits

Per-key limit per minute (default 60). Every response includes X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset. Exceeding it returns 429 with Retry-After.

Idempotency

Send an Idempotency-Key header on POST requests. Retrying with the same key and body returns the original response (Idempotent-Replayed: true); a different body returns 422.

Errors

{"error": {"code": 422, "message": "A valid email address is required."}}

401 missing/invalid key · 403 scope or plan does not allow the action · 404 not found (including resources of other workspaces) · 409 conflict · 422 validation · 429 rate limited.

Contacts

GET/contactscontacts:read

List contacts. Query: page, per_page (≤100), email, status.

Response
{"data":[{"id":12,"email":"ana@example.com","first_name":"Ana","status":"subscribed","lists":[3],"tags":["vip"],"custom_fields":{"city":"Lisbon"},"created_at":"2026-05-01 10:00:00"}],"meta":{"page":1,"per_page":25,"total":1}}
POST/contactscontacts:write

Create a contact. consent is required: opt_in (you already hold permission) or double_opt_in (we email a confirmation link; status stays pending). Set upsert: true to update an existing contact. Existing unsubscribed/bounced contacts are never re-subscribed via the API.

Request
curl -X POST https://mailsaas.searchpilot.store/api/v1/contacts \ -H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \ -d '{"email":"ana@example.com","first_name":"Ana","list_ids":[3],"tags":["vip"],"custom_fields":{"city":"Lisbon"},"consent":"opt_in","consent_ip":"203.0.113.7","consent_note":"Checkout opt-in checkbox"}'
Response
{"data":{"id":12,"email":"ana@example.com","status":"subscribed", "...": "..."}} (201 Created)
GET/contacts/{id}contacts:read

Retrieve a contact.

Response
{"data":{"id":12,"email":"ana@example.com","...":"..."}}
PUT/contacts/{id}contacts:write

Update fields, lists and tags. status may only be set to unsubscribed.

Request
curl -X PUT https://mailsaas.searchpilot.store/api/v1/contacts/{id} \ -H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \ -d '{"first_name":"Ana Maria","tags":["vip","customer"]}'
Response
{"data":{"id":12,"first_name":"Ana Maria","...":"..."}}
DELETE/contacts/{id}contacts:write

Delete a contact.

Response
(204 No Content)

Campaigns

GET/campaignscampaigns:read

List campaigns. Query: page, per_page, status.

Response
{"data":[{"id":7,"name":"May newsletter","status":"completed","total_recipients":5120}],"meta":{"page":1,"per_page":25,"total":1}}
POST/campaignscampaigns:write

Create a draft campaign. HTML is sanitized and merge tags validated.

Request
curl -X POST https://mailsaas.searchpilot.store/api/v1/campaigns \ -H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \ -d '{"name":"May newsletter","subject":"Hi {{first_name}}","from_name":"Acme","from_email":"news@acme.com","list_ids":[3],"html":"<html>...<a href=\"{{unsubscribe_url}}\">Unsubscribe</a></html>"}'
Response
{"data":{"id":7,"status":"draft","...":"..."}} (201)
GET/campaigns/{id}campaigns:read

Retrieve a campaign including HTML.

Response
{"data":{"id":7,"html":"<html>...","...":"..."}}
POST/campaigns/{id}/sendcampaigns:write

Validate and queue (or schedule with scheduled_at, UTC). Sending happens in background workers. Repeated calls cannot send twice (409).

Request
curl -X POST https://mailsaas.searchpilot.store/api/v1/campaigns/{id}/send \ -H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \ -d '{"scheduled_at":"2026-06-01 09:00:00"}'
Response
{"data":{"id":7,"status":"scheduled","...":"..."}} (202)

Reports

GET/reports/{campaign_id}campaigns:read

Campaign statistics. Opens are approximate.

Response
{"data":{"campaign_id":7,"accepted":5100,"delivered":5032,"bounced":21,"unique_opens":2011,"unique_clicks":388,"unsubscribed":9,"rates":{"delivery":98.67,"open":39.96,"click":7.71,"bounce":0.41}}}

Automations

GET/automationsautomations:read

List workflows.

Response
{"data":[{"id":4,"name":"Welcome","status":"active","trigger_type":"contact_subscribed","version":2}]}
POST/automationsautomations:write

Create a linear workflow (use the visual builder for branches). Step types: send_email, add_tag, remove_tag, add_to_list, remove_from_list, update_contact, wait.

Request
curl -X POST https://mailsaas.searchpilot.store/api/v1/automations \ -H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \ -d '{"name":"Welcome","trigger":{"type":"added_to_list","list_id":3},"steps":[{"type":"send_email","template_id":5,"subject":"Welcome!","from_email":"hi@acme.com"},{"type":"wait","amount":2,"unit":"days"},{"type":"add_tag","tag_id":9}],"activate":true}'
Response
{"data":{"id":4,"status":"active","version":1}} (201)
POST/eventscontacts:write

Fire an API-event trigger for a contact. Optional event_id deduplicates repeated deliveries.

Request
curl -X POST https://mailsaas.searchpilot.store/api/v1/events \ -H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \ -d '{"email":"ana@example.com","event":"purchase","event_id":"order-1001"}'
Response
{"data":{"event":"purchase","contact_id":12,"workflows_started":1}} (202)

Account

GET/account/creditsaccount:read

Wallet balance and subscription.

Response
{"data":{"balance":20000,"reserved":120,"available":19880,"subscription":{"plan":"Starter","status":"active","billing_cycle":"monthly","current_period_end":"2026-06-01 00:00:00"}}}

Outbound webhooks

Configure endpoints in Workspace → Webhooks. Events: email.delivered, email.bounce, email.complaint, email.open, email.click, email.unsubscribe, campaign.completed, contact.created. Verify X-Signature: t=TIMESTAMP,v1=SIGNATURE where SIGNATURE = hex HMAC-SHA256 of TIMESTAMP + "." + raw_body using your endpoint secret.

{"id":"9f2c…","event":"email.click","created":1767225600,"data":{"campaign_id":7,"contact_id":12,"email":"ana@example.com","url":"https://acme.com/offer"}}