# Splashify Pro Partner API — Complete Documentation > This is the full-text version of the Splashify Pro partner documentation. > Generated for AI consumption. Source: https://partner-docs.splashifypro.com > Generated at: 2026-06-24T04:04:45.214Z > Total pages: 121 --- ======================================================================== ## Splashify Pro Email API URL: https://partner-docs.splashifypro.com/ ======================================================================== # Splashify Pro Email API Splashify Pro is an email API for developers. The partner API gives you the building blocks to ship transactional and marketing email at scale: verified sending identities, configuration sets, templates, suppression lists, real-time webhooks, and deliverability-grade reputation tracking. The API is shaped like AWS SES. If you've integrated against AWS SES before, you'll feel at home — sending an email, configuring an event destination, or polling send statistics each map onto a familiar endpoint with the same field names. ## Base URL All API requests are made to: ``` https://api.splashifypro.com/api/v1/partner/email ``` ## Authentication Every API request must carry your secret API key in the `Authorization` header: ```bash Authorization: Bearer pk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ``` Generate a key from your partner dashboard at [partner.splashifypro.com](https://partner.splashifypro.com) under **Settings → API Keys**. The key is shown once — store it in a secure secret manager. > **Security:** Treat your API key like a password. Never embed it in > client-side code, mobile apps, or public repositories. ## Quick example Send a transactional email with two lines of curl: ```bash curl https://api.splashifypro.com/api/v1/partner/email/send \ -H "Authorization: Bearer $SPLASHIFY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": "alerts@yourcompany.com", "to": ["customer@example.com"], "subject": "Your order has shipped", "html_body": "
Tracking: ABC123
", "text_body": "Tracking: ABC123" }' ``` Response: ```json { "success": true, "results": [ { "recipient": "customer@example.com", "message_id": "f9c3a2b1-...", "status": "queued" } ] } ``` ## API surface at a glance | Action | Endpoint | AWS SES equivalent | |---|---|---| | Send transactional | `POST /send` | `SendEmail` | | Send raw MIME | `POST /send-raw` | `SendRawEmail` | | Send templated | `POST /send-template` | `SendTemplatedEmail` | | Bulk templated | `POST /send-bulk` | `SendBulkTemplatedEmail` | | Verify domain / address | `POST /identities` | `CreateEmailIdentity` | | Configuration sets | `/configuration-sets` | `CreateConfigurationSet`... | | Event destinations | `/configuration-sets/:id/event-destinations` | `CreateConfigurationSetEventDestination` | | Templates | `/templates` | `CreateEmailTemplate`... | | Suppression list | `/suppression` | `PutSuppressedDestination`... | | Send quota | `GET /quotas` | `GetSendQuota` | | Send statistics | `GET /stats` | `GetSendStatistics` | | Reputation | `GET /reputation` | `GetAccountReputation` | | Production access | `POST /production-access` | Submit support case | ## Response format Success responses always include `success: true`: ```json { "success": true, "data": { ... } } ``` Error responses carry a stable `error` code + a human-readable `message`: ```json { "success": false, "error": "INVALID_REQUEST", "message": "from address is not on a verified identity" } ``` ## HTTP status codes | Code | Meaning | |---|---| | `200` | Success | | `201` | Resource created | | `400` | Bad request — fix your inputs | | `401` | Missing / invalid API key | | `402` | Insufficient wallet balance — top up | | `403` | Sending paused, sandbox cap reached, or feature locked | | `404` | Resource not found | | `409` | Conflict (duplicate name, etc.) | | `429` | Rate limit hit — back off | | `500` | Server error — retry with backoff | | `503` | Database / dependency unavailable | ## Get started - [**Getting Started →**](/getting-started) — first-send walkthrough - [**Concepts →**](/concepts) — sending identities, config sets, sandbox - [**API Reference →**](/api-reference) — every endpoint - [**Webhooks →**](/webhooks) — receive delivery events - [**Deliverability →**](/deliverability) — SPF/DKIM/DMARC and reputation - [**Pricing →**](/pricing) — flat ₹0.01 per email ======================================================================== ## Getting Started URL: https://partner-docs.splashifypro.com/getting-started ======================================================================== # Getting Started This guide walks you through the steps to send your first email through the Splashify Pro Email API. ## Prerequisites 1. A Splashify Pro partner account ([sign up free](https://partner.splashifypro.com/auth/signup)) 2. An API key (generated from your partner dashboard) 3. A domain you control (for production sends — sandbox sends work without a verified domain but only to addresses you've verified) ## Step 1 — Sign up and get an API key If you don't have an account: 1. Visit [partner.splashifypro.com/auth/signup](https://partner.splashifypro.com/auth/signup) 2. Enter your email, mobile number (with country code), and a password 3. Verify the OTP sent to **both** your email and WhatsApp 4. Log in To create an API key: 1. Go to **Settings → API Keys** 2. Click **Generate API Key** 3. Copy and securely store the key — it is only shown once 4. Use it as the Bearer token in the `Authorization` header on every API request ## Step 2 — Verify a sending identity Before you can send from an address, you must verify ownership of the domain or the email address itself. Domain verification is preferred because it covers any address at that domain. ```bash curl https://api.splashifypro.com/api/v1/partner/email/identities \ -H "Authorization: Bearer $SPLASHIFY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "identity_type": "DOMAIN", "identity_value": "yourcompany.com" }' ``` The response includes the three DNS records you need to publish: ```json { "success": true, "identity_type": "DOMAIN", "identity_value": "yourcompany.com", "status": "PENDING", "dns_records": { "spf": { "type": "TXT", "hostname": "yourcompany.com", "value": "v=spf1 include:_spf.mail.splashifypro.com ~all" }, "dkim": { "type": "CNAME", "hostname": "splashify._domainkey.yourcompany.com", "value": "splashify._domainkey.mail.splashifypro.com" }, "dmarc": { "type": "TXT", "hostname": "_dmarc.yourcompany.com", "value": "v=DMARC1; p=quarantine; rua=mailto:dmarc@splashifypro.com" } } } ``` Publish all three records on your DNS provider and trigger a re-check: ```bash curl -X POST https://api.splashifypro.com/api/v1/partner/email/identities/DOMAIN/yourcompany.com/verify \ -H "Authorization: Bearer $SPLASHIFY_API_KEY" ``` When `"status": "VERIFIED"` comes back, you can send from any address ending in `@yourcompany.com`. ## Step 3 — Send a transactional email ```bash curl https://api.splashifypro.com/api/v1/partner/email/send \ -H "Authorization: Bearer $SPLASHIFY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": "hello@yourcompany.com", "to": ["customer@example.com"], "subject": "Welcome", "html_body": "Thanks for signing up.
", "text_body": "Welcome aboard. Thanks for signing up." }' ``` The response carries the `message_id` you can use to poll delivery status: ```json { "success": true, "results": [ { "recipient": "customer@example.com", "message_id": "550e8400-e29b-41d4-a716-446655440000", "status": "queued" } ] } ``` ## Step 4 — Watch delivery status Poll the message status: ```bash curl https://api.splashifypro.com/api/v1/partner/email/emails/550e8400-e29b-41d4-a716-446655440000 \ -H "Authorization: Bearer $SPLASHIFY_API_KEY" ``` Or — better — set up a [webhook](/webhooks) that we POST to whenever the message transitions through `Send → Delivery → Open → Click / Bounce / Complaint`. ## Step 5 — Move out of sandbox New accounts start in **sandbox mode**: - 200 emails/day cap - 1 email/sec peak send rate - Can only send to verified-recipient addresses To go live, request **production access**: ```bash curl -X POST https://api.splashifypro.com/api/v1/partner/email/production-access \ -H "Authorization: Bearer $SPLASHIFY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "use_case": "Transactional emails for our SaaS application: signup confirmations, password resets, payment receipts.", "email_volume_estimate": "5000-15000 per day", "has_unsubscribe_method": true, "has_consent_proof": true }' ``` Approved requests lift sandbox + bump your daily quota to 50,000 + peak rate to 14/sec. Most requests are reviewed within 24 business hours. ## Next steps - [**Authentication →**](/getting-started/authentication) — API key best practices - [**Concepts →**](/concepts) — configuration sets, event destinations, suppression - [**Webhooks →**](/webhooks) — real-time delivery events - [**API Reference →**](/api-reference) — full endpoint reference ======================================================================== ## Authentication URL: https://partner-docs.splashifypro.com/getting-started/authentication ======================================================================== # Authentication The Splashify Pro Email API uses Bearer-token authentication. Every request must carry a valid API key in the `Authorization` header. ## Generating an API key 1. Log in to [partner.splashifypro.com](https://partner.splashifypro.com) 2. Navigate to **Settings → API Keys** 3. Click **Generate API Key** 4. Copy the key — it is shown **once**. Lose it and you'll need to regenerate. API keys carry the prefix `pk_live_` and are 64 characters long. ## Using your key Set the `Authorization` header on every request: ```http Authorization: Bearer pk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ``` cURL: ```bash curl https://api.splashifypro.com/api/v1/partner/email/quotas \ -H "Authorization: Bearer pk_live_..." ``` Node: ```js fetch("https://api.splashifypro.com/api/v1/partner/email/quotas", { headers: { Authorization: `Bearer ${process.env.SPLASHIFY_API_KEY}` }, }); ``` Python: ```python r = requests.get( "https://api.splashifypro.com/api/v1/partner/email/quotas", headers={"Authorization": f"Bearer {os.environ['SPLASHIFY_API_KEY']}"}, ) ``` ## Rate limits API keys are rate-limited per partner account, not per key. Defaults: - **Sandbox:** 1 send/sec, 200 sends/day - **Production:** 14 sends/sec (configurable per partner), 50,000 sends/day (configurable per partner) Rate-limit responses come back as `429 Too Many Requests`. Retry with exponential backoff. ## Key security best practices - **Never embed in client-side code.** API keys go on your server, never in browser JS, mobile apps, or public repos. - **Use environment variables.** Most CI / hosting platforms support secret env vars. `.env` files should be `.gitignore`'d. - **Rotate periodically.** Regenerate keys every 90 days at minimum. - **Use one key per environment.** Separate keys for staging / production make blast-radius cleanup easier. ## Revoking a compromised key 1. Go to **Settings → API Keys** 2. Find the compromised key 3. Click **Revoke** Revocation is immediate. New requests with the revoked key get `401 Unauthorized` within ~5 seconds. ## Authentication errors | Status | Code | Cause | |---|---|---| | 401 | `MISSING_AUTH` | No `Authorization` header | | 401 | `INVALID_KEY_FORMAT` | Key doesn't match `pk_live_...` | | 401 | `KEY_NOT_FOUND` | Key was revoked or never existed | | 401 | `KEY_INACTIVE` | Account suspended | | 403 | `IP_BLOCKED` | Caller's IP is on your account's IP allowlist | ======================================================================== ## cURL Quickstart URL: https://partner-docs.splashifypro.com/getting-started/curl-quickstart ======================================================================== # cURL Quickstart The fastest way to test an integration. Every endpoint can be hit directly with cURL — useful for debugging, scripts, and CI smoke tests. ## Set your API key ```bash export SPLASHIFY_API_KEY="pk_live_..." ``` ## Send an email ```bash curl https://api.splashifypro.com/api/v1/partner/email/send \ -H "Authorization: Bearer $SPLASHIFY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": "hello@yourcompany.com", "to": ["customer@example.com"], "subject": "Welcome", "html_body": "Your account is ready.
", text_body: "Welcome. Your account is ready.", }), }, ); const data = await res.json(); console.log(data.results[0].message_id); ``` ## 4. Handle errors ```js if (!res.ok) { const err = await res.json(); switch (err.error) { case "INVALID_REQUEST": // Bad input — read err.message break; case "FROM_NOT_VERIFIED": // Verify your domain at /identities first break; case "SUPPRESSED_RECIPIENT": // Recipient is on your suppression list break; case "INSUFFICIENT_BALANCE": // Recharge your wallet break; default: console.error(err.message); } } ``` ## 5. With React Email [React Email](https://react.email) renders HTML email from React components. You author your template as JSX, render it server-side, and pass the HTML to `/send`. ```bash npm install @react-email/components @react-email/render ``` ```jsx function Welcome({ name }) { return (Your account is ready.
", text_body: "Welcome. Your account is ready." }} pathParams={[]} /> ## What happens on send ```mermaid sequenceDiagram participant You as Your app participant API as Splashify API participant MX as Recipient MX participant Hook as Your webhook You->>API: POST /partner/email/send API-->>You: 200 { message_id, status: queued } API->>MX: STARTTLS + DKIM-signed message MX-->>API: 250 OK API->>Hook: POST event (Send) API->>Hook: POST event (Delivery) Note over MX,Hook: Bounce / Complaint events follow async ``` The API responds immediately with a `message_id`. Behind the scenes we: 1. **Look up your verified identity.** From-address must be on a verified domain (or be a verified email address). 2. **Check the suppression list.** Recipients on your account's suppression list are rejected with `status: rejected` — no SMTP attempt is made and you're not billed. 3. **Deduct ₹0.01 from your wallet.** First 200/day are free in sandbox. 4. **Sign with DKIM** using a key that resolves through your domain's CNAME at `splashify._domainkey.Welcome aboard.
" }' ``` ## What's next - **Verify your domain →** [Identities](/api-reference/identities/create) - **Set up webhooks →** [Webhooks](/webhooks) - **Use templates →** [Templates](/api-reference/templates/create) - **Bulk send →** [SendBulk](/api-reference/emails/send-bulk) - **Move out of sandbox →** [Production access](/api-reference/production-access/submit) ======================================================================== ## SMTP Relay URL: https://partner-docs.splashifypro.com/getting-started/smtp-quickstart ======================================================================== # SMTP Relay For frameworks and platforms that can't easily call our REST API, use the SMTP relay. Same DKIM signing, suppression, billing, and webhook delivery — your app just speaks plain SMTP. ## Connection | Setting | Value | |---|---| | Host | `smtp.splashifypro.com` | | Port | `587` (STARTTLS) or `465` (TLS) | | Username | `emailapikey` (literal) | | Password | `pk_live_...` (your API key) | TLS is required. Plain-text auth is rejected. ## Test with swaks ```bash swaks --to customer@example.com \ --from hello@yourcompany.com \ --server smtp.splashifypro.com \ --port 587 -tls \ --auth-user emailapikey \ --auth-password "$SPLASHIFY_API_KEY" \ --header "Subject: Test from swaks" \ --body "Hello" ``` ## WordPress (WP Mail SMTP) 1. Plugin → **WP Mail SMTP** → install 2. Mailer → **Other SMTP** 3. Host: `smtp.splashifypro.com` · Port: `587` · Encryption: `TLS` 4. Auth: ON · Username: `emailapikey` · Password: your API key 5. Save & send a test email ## Django ```python # settings.py EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" EMAIL_HOST = "smtp.splashifypro.com" EMAIL_PORT = 587 EMAIL_USE_TLS = True EMAIL_HOST_USER = "emailapikey" EMAIL_HOST_PASSWORD = os.environ["SPLASHIFY_API_KEY"] DEFAULT_FROM_EMAIL = "hello@yourcompany.com" ``` ## Laravel ```env MAIL_MAILER=smtp MAIL_HOST=smtp.splashifypro.com MAIL_PORT=587 MAIL_USERNAME=emailapikey MAIL_PASSWORD=${SPLASHIFY_API_KEY} MAIL_ENCRYPTION=tls MAIL_FROM_ADDRESS=hello@yourcompany.com ``` ## Node (nodemailer) ```js const transport = nodemailer.createTransport({ host: "smtp.splashifypro.com", port: 587, secure: false, // STARTTLS auth: { user: "emailapikey", pass: process.env.SPLASHIFY_API_KEY, }, }); await transport.sendMail({ from: "hello@yourcompany.com", to: "customer@example.com", subject: "Hello", html: "Welcome
", }); ``` ## Postfix In `/etc/postfix/main.cf`: ``` relayhost = [smtp.splashifypro.com]:587 smtp_sasl_auth_enable = yes smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd smtp_sasl_security_options = noanonymous smtp_use_tls = yes smtp_tls_security_level = encrypt ``` In `/etc/postfix/sasl_passwd`: ``` [smtp.splashifypro.com]:587 emailapikey:pk_live_... ``` ```bash postmap /etc/postfix/sasl_passwd chmod 600 /etc/postfix/sasl_passwd* postfix reload ``` ## Supabase Auth (transactional email for signups, OTPs, password reset) Supabase ships a "Custom SMTP" panel that you can point at the relay so signup confirmation, magic-link, and password-recovery emails come from your verified sender instead of the Supabase default. **Project → Project Settings → Auth → SMTP Settings:** | Field | Value | |---|---| | Sender email | `noreply@yourcompany.com` *(must be on a verified identity)* | | Sender name | Whatever appears in the inbox From line | | Host | `smtp.splashifypro.com` | | Port | `587` | | Username | `emailapikey` | | Password | your `pk_live_…` API key | | Minimum interval per user | `60` seconds (Supabase default — anti-abuse) | Then save & hit **Send test email**. If you get `535 5.0.0 5.7.0 invalid api key format` it means you pasted something other than a `pk_live_…` (or `sk_live_…` for app-developer accounts) key — copy the key from [Settings → API key](https://partner.splashifypro.com/settings) on the partner dashboard. ## Things to know - **Sender must be on a verified identity.** Sends from an unverified email/domain are rejected with `550 5.7.1 sender 'Invoice attached.
", "attachments": [ { "filename": "invoice.pdf", "content_type": "application/pdf", "content_base64": "See attached.
" }); msg.addAttachment({ filename: "invoice.pdf", contentType: "application/pdf", data: pdfBase64, }); const raw = msg.asEncoded(); const rawB64 = Buffer.from(raw).toString("base64"); await fetch("https://api.splashifypro.com/api/v1/partner/email/send-raw", { method: "POST", headers: { Authorization: `Bearer ${API_KEY}` }, body: JSON.stringify({ raw_message_base64: rawB64 }), }); ``` ======================================================================== ## Send template URL: https://partner-docs.splashifypro.com/api-reference/emails/send-template ======================================================================== # Send template Send a previously-saved [template](/api-reference/templates/create) with per-recipient `{{variables}}` substituted in the subject + body. ```http POST /api/v1/partner/email/send-template ``` ## Request body ```json { "from": "hello@yourcompany.com", "to": ["customer@example.com"], "template_name": "welcome", "variables": { "first_name": "Alex", "company_name": "Acme" }, "configuration_set_name": "production" } ``` | Field | Type | Required | Notes | |---|---|---|---| | `from` | string | yes | Verified identity | | `to` | string[] | yes | Up to 50 recipients | | `cc` / `bcc` | string[] | no | | | `reply_to` | string | no | | | `template_name` | string | yes | Friendly name from `POST /templates` | | `variables` | object | no | `{{key}}` tokens replaced in subject + body | | `configuration_set_name` | string | no | Event-routing scope | | `customer_id` | string | no | Downstream customer attribution | | `category` | string | no | `transactional` (default) or `marketing` | ## Response Same shape as [`/send`](/api-reference/emails/send). ## Variable substitution `{{first_name}}` and `{{ first_name }}` (with spaces) both resolve. Missing variables stay as-is in the rendered output — they don't raise errors. To enforce required variables, declare them on the template via `declared_vars`. ## cURL ```bash curl https://api.splashifypro.com/api/v1/partner/email/send-template \ -H "Authorization: Bearer $SPLASHIFY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": "hello@yourcompany.com", "to": ["customer@example.com"], "template_name": "welcome", "variables": {"first_name": "Alex"} }' ``` ## Common errors | Status | Code | Meaning | |---|---|---| | 404 | `TEMPLATE_NOT_FOUND` | `template_name` not registered for this account | | 400 | `RENDERING_FAILURE` | Template rendering failed (rare — most variables are forgiving) | ======================================================================== ## Send with attachments URL: https://partner-docs.splashifypro.com/api-reference/emails/send-with-attachments ======================================================================== # Send with attachments Three of the four send endpoints accept an `attachments[]` array — `/send`, `/send-template`, and `/send-bulk`. Each attachment carries the file bytes inline as base64; we build the `multipart/mixed` MIME structure for you, DKIM-sign it, and ship it to the recipient through the same pipeline as plain-body emails. `/send-raw` always supported attachments because partners build the MIME themselves — see [Send raw](/api-reference/emails/send-raw) for that path. ## Attachment object Each entry in `attachments[]` is shaped like this: ```json { "filename": "invoice-2026-04.pdf", "content_type": "application/pdf", "content_base64": "Hi! Your invoice for March is attached.
\", \"attachments\": [ { \"filename\": \"invoice-march-2026.pdf\", \"content_type\": \"application/pdf\", \"content_base64\": \"$ATT\" } ] }" ``` ## Example — Node.js (multiple files) ```js const toAtt = (path, contentType) => ({ filename: path.split('/').pop(), content_type: contentType, content_base64: readFileSync(path).toString('base64'), }) const r = await fetch('https://api.splashifypro.com/api/v1/partner/email/send', { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.SPLASHIFY_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ from: 'billing@yourcompany.com', to: ['customer@example.com'], subject: 'Your March documents', html_body: 'Attached: invoice + receipt + signed contract.
', attachments: [ toAtt('./invoice.pdf', 'application/pdf'), toAtt('./receipt.pdf', 'application/pdf'), toAtt('./contract.pdf', 'application/pdf'), ], }), }) console.log(await r.json()) ``` ## Example — Python ```python def att(path, ct): with open(path, 'rb') as f: return { 'filename': os.path.basename(path), 'content_type': ct, 'content_base64': base64.b64encode(f.read()).decode(), } r = requests.post( 'https://api.splashifypro.com/api/v1/partner/email/send', headers={'Authorization': f"Bearer {os.environ['SPLASHIFY_API_KEY']}"}, json={ 'from': 'support@yourcompany.com', 'to': ['customer@example.com'], 'subject': 'Attached photo', 'html_body': 'Here\'s the photo from your visit.
', 'attachments': [att('./photo.jpg', 'image/jpeg')], }, ) print(r.json()) ``` ## Example — Inline image (CID reference) When you want the image to render **inside** the email body instead of as a separate downloadable file, use `inline: true` + `content_id`, then reference it from your HTML with `cid:`: ```json { "from": "newsletter@yourcompany.com", "to": ["customer@example.com"], "subject": "Today's newsletter", "html_body": "Welcome!
Thanks for joining {{company_name}}.
", "text": "Hi {{first_name}}. Thanks for joining {{company_name}}.", "declared_vars": ["first_name", "company_name"] } ``` | Field | Type | Required | Notes | |---|---|---|---| | `template_name` | string | yes | Unique per partner. Lowercase letters / numbers / `_` `-`, max 64 chars | | `subject` | string | yes | Variables can be used here too | | `html` | string | conditional | One of `html` or `react_email_json` is required | | `text` | string | no | Plaintext fallback. Auto-derived from HTML if omitted | | `react_email_json` | string | conditional | Visual-editor JSON. Renders to `html` + `text` server-side | | `declared_vars` | string[] | no | Variable names the template uses. Surfaced on the panel + helps catch typos | ## Variable syntax Both `{{var_name}}` and `{{ var_name }}` (with spaces) work. Missing variables at send time stay as the literal `{{name}}` in the rendered output rather than raising an error — this keeps hot-path sends forgiving. To enforce required variables, declare them in `declared_vars` and validate yourself before calling `/send-template`. ## Response ```json { "success": true, "template": { "template_id": "tpl_550e8400-...", "template_name": "welcome", "subject": "Welcome to {{company_name}}, {{first_name}}", "html": "Hi {{first_name}}
" }' ``` ## Common errors | Status | Code | Meaning | |---|---|---| | 409 | `TEMPLATE_NAME_TAKEN` | Another template with that name exists | | 400 | `INVALID_REQUEST` | Bad name format or missing both `html` and `react_email_json` | | 400 | `TEMPLATE_LIMIT_REACHED` | 500-template cap per account | ======================================================================== ## Delete template URL: https://partner-docs.splashifypro.com/api-reference/templates/delete ======================================================================== # Delete template ```http DELETE /api/v1/partner/email/templates/:id ``` Removes the template + its name alias. In-flight `/send-template` calls referencing this template by name return `404 TEMPLATE_NOT_FOUND` after the delete commits. ## cURL ```bash curl -X DELETE \ https://api.splashifypro.com/api/v1/partner/email/templates/tpl_550e8400-... \ -H "Authorization: Bearer $SPLASHIFY_API_KEY" ``` ## What this does NOT do - Does NOT recall already-queued sends that referenced this template. - Does NOT remove historical send records — `GET /emails/:message_id` still returns the original `template_id` for past sends. ======================================================================== ## Get template URL: https://partner-docs.splashifypro.com/api-reference/templates/get ======================================================================== # Get template ```http GET /api/v1/partner/email/templates/:id ``` Fetch full template content by template_id. ## Response ```json { "success": true, "template": { "template_id": "tpl_550e8400-...", "template_name": "welcome", "subject": "Welcome", "html": "Hi
", "text_body": "Hi", "configuration_set_name": "production", "category": "transactional" } ``` Response: `{"success":true,"results":[{"recipient":"user@example.com","message_id":"", message: ""}` with a 4xx or 5xx status:
- `400` — bad request (read `message`)
- `401` — invalid / missing API key
- `402` — wallet balance insufficient (recharge)
- `403` — sandbox cap, sending paused, IP blocked
- `404` — not found
- `429` — rate limit (back off)
## Where to read more
- Full API reference: https://partner-docs.splashifypro.com/api-reference
- Webhooks: https://partner-docs.splashifypro.com/webhooks
- Deliverability: https://partner-docs.splashifypro.com/deliverability
- Knowledge base: https://partner-docs.splashifypro.com/concepts
That's everything an AI agent needs to start integrating. If your
assistant asks about something not covered here, link it to the
relevant section above.
========================================================================
## MCP Server
URL: https://partner-docs.splashifypro.com/build-with-ai/mcp-server
========================================================================
# MCP Server
The **Splashify Pro MCP server** lets your AI assistant (Claude
Desktop, Cursor, Windsurf, Claude Code, etc.) call the Email API
directly. Send emails, verify identities, configure webhooks — all
from a chat conversation.
[Model Context Protocol](https://modelcontextprotocol.io) is the
open standard Anthropic shipped for connecting AI tools to external
APIs. Splashify Pro hosts an MCP server that exposes every public
endpoint as a tool the assistant can call.
## Endpoint
The MCP server is **hosted** — there's nothing to install locally.
Point your AI client at the SSE endpoint and authenticate with your
partner API key:
```
URL: https://mcp.splashifypro.com/sse
Auth: Authorization: Bearer pk_live_...
```
Use the same `pk_live_…` key you use for the REST API (generate one
at [partner.splashifypro.com](https://partner.splashifypro.com) →
Settings → API Key). The MCP server uses the same auth chain,
permission scope, and rate-limit budget as REST.
## Add to Claude Desktop
Edit `~/Library/Application Support/Claude/claude_desktop_config.json`
(macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
```json
{
"mcpServers": {
"splashifypro": {
"type": "sse",
"url": "https://mcp.splashifypro.com/sse",
"headers": {
"Authorization": "Bearer pk_live_..."
}
}
}
}
```
Restart Claude Desktop. You'll see "splashifypro" in the bottom-left
tools menu.
## Add to Cursor
Edit `.cursor/mcp.json` in your project root:
```json
{
"mcpServers": {
"splashifypro": {
"type": "sse",
"url": "https://mcp.splashifypro.com/sse",
"headers": {
"Authorization": "Bearer pk_live_..."
}
}
}
}
```
## Add to Claude Code
Edit `~/.claude/settings.json` or your project's `.mcp.json`:
```json
{
"mcpServers": {
"splashifypro": {
"type": "sse",
"url": "https://mcp.splashifypro.com/sse",
"headers": {
"Authorization": "Bearer pk_live_..."
}
}
}
}
```
## Add to Windsurf
Edit `~/.codeium/windsurf/mcp_config.json`. Same JSON shape as
Cursor.
## Available tools
The MCP server exposes 35+ tools, one per Email API endpoint:
| Category | Tools |
|---|---|
| Send | `partner_email_send`, `partner_email_send_template`, `partner_email_send_bulk`, `partner_email_send_raw`, `partner_email_get_status` |
| Identities | `partner_email_list_identities`, `partner_email_create_identity`, `partner_email_get_identity`, `partner_email_verify_identity`, `partner_email_delete_identity` |
| Configuration sets | `partner_email_list_configuration_sets`, `partner_email_create_configuration_set`, `partner_email_get_configuration_set`, `partner_email_delete_configuration_set` |
| Event destinations | `partner_email_create_event_destination` |
| Templates | `partner_email_list_templates`, `partner_email_create_template`, `partner_email_delete_template`, `partner_email_preview_template` |
| Suppression | `partner_email_list_suppression`, `partner_email_add_suppression`, `partner_email_remove_suppression` |
| Stats + reputation | `partner_email_get_quotas`, `partner_email_get_stats`, `partner_email_get_reputation`, `partner_email_list_events`, `partner_email_get_bounce_report` |
| Production access | `partner_email_list_production_access_requests`, `partner_email_submit_production_access` |
| Dedicated IP | `partner_email_get_dedicated_ip_request`, `partner_email_request_dedicated_ip`, `partner_email_cancel_dedicated_ip_request` |
| Account + security | `partner_email_list_activity_logs`, `partner_email_list_ip_allowlist`, `partner_email_add_ip_allowlist_entry`, `partner_email_remove_ip_allowlist_entry` |
## Example prompts
> "Send a welcome email to alex@example.com from hello@mycompany.com"
The assistant calls `partner_email_send` with the right shape, hands
the `message_id` back to you, and offers to follow up via
`partner_email_get_status`.
> "Verify the domain mycompany.com and tell me which DNS records I need"
`partner_email_create_identity` is called, the response includes the
SPF / DKIM / DMARC records, and the assistant explains where to
publish them.
> "Why are 5% of my emails bouncing?"
`partner_email_get_reputation` + `partner_email_list_events?event_type=bounce`
fire in parallel; the assistant correlates the bounces against
recipients and suggests fixes.
## Security
- **Hosted, not local.** Your API key never leaves your machine
except as a Bearer header on outbound HTTPS requests to
`mcp.splashifypro.com` — same path your REST calls already take.
- **Mutating tools confirm first.** Tools that change state
(`partner_email_send`, `partner_email_create_*`,
`partner_email_delete_*`) generally require the assistant to
read back the action; you approve before they fire.
- **Audit log.** Every MCP-driven call shows up at
[partner.splashifypro.com](https://partner.splashifypro.com) →
Activity Log alongside REST + SDK calls so you can see who/what
fired which endpoint when.
- **Rate limits.** MCP calls share the same per-key budget as REST.
Burst protection kicks in identically.
## Troubleshooting
- **"splashifypro server not found":** check the JSON config path +
restart the host app. SSE transport requires the host app to
support remote MCP (Claude Desktop ≥ 0.7, Cursor ≥ 0.42, Claude
Code ≥ 1.0).
- **`401 invalid_key`:** API key is wrong or revoked. Generate a
fresh one at the partner panel.
- **`402 insufficient_balance`:** wallet is empty — recharge from
the [partner panel](https://partner.splashifypro.com/wallet).
- **`429 rate_limited`:** assistant is firing too many requests
too fast — back off or apply for production access to lift the
per-second cap.
## Feedback + bugs
Spotted a bug or want a tool that isn't listed? Email
**support@splashifypro.in** with the prompt that triggered it and
your `pk_live_` key prefix (first 12 chars). We ship MCP fixes
weekly.
========================================================================
## Common Recipes
URL: https://partner-docs.splashifypro.com/build-with-ai/recipes
========================================================================
# Common Recipes
Production-grade patterns that save you reinventing the wheel.
## Idempotent transactional sends
Replays of the same logical send (e.g. payment-receipt for the same
charge ID) shouldn't trigger duplicate emails. Stamp a stable
`X-Idempotency-Key` header derived from your business key, and
de-dup on your side before calling `/send`:
```js
const sentKey = await redis.get(`email:sent:${chargeId}`);
if (sentKey) return; // already emailed
const res = await client.emails.send({...});
await redis.set(`email:sent:${chargeId}`, res.results[0].messageId, { EX: 86400 });
```
We don't currently honor an `Idempotency-Key` header server-side
(roadmap), so the dedup happens at your layer.
## Per-recipient personalization at scale
For bulk sends with per-recipient variables, use `/send-bulk`:
```js
await client.emails.sendBulk({
from: "hello@yourcompany.com",
templateName: "newsletter-june",
defaultTemplateData: { campaign: "june-2026" },
destinations: users.map(u => ({
to: [u.email],
replacementData: {
first_name: u.firstName,
unsubscribe_url: `https://yoursite.com/u/${u.token}`,
},
})),
});
```
50 destinations × 50 recipients × 500 total per request. For
larger volumes, chunk into multiple calls — there's no per-account
rate limit on bulk-send concurrency, only the per-account per-second
peak rate.
## Dynamic from-name
Set the `from` field as `"Display Name "` and
recipients see "Display Name" in their inbox:
```json
{
"from": "Sarah from Acme ",
...
}
```
Domain still has to be verified. Display name is unverified —
recipients see whatever you put there.
## Custom MAIL FROM domain (return-path)
Sets the bounce return-path to a custom subdomain so DMARC alignment
includes both DKIM and SPF. Lands on roadmap. Until then, the
return-path is `bounces@mail.splashifypro.com` and DMARC alignment
relies on DKIM only (`p=quarantine` is fine; `p=reject` may need
DMARC relaxed alignment).
## Per-customer attribution
If you're sending on behalf of multiple downstream customers, create
a configuration set per customer and reference it on every send:
```js
await client.emails.send({
from: "alerts@yourcompany.com",
to: ["customer@example.com"],
configurationSetName: "customer_acme_corp",
...
});
```
Stats roll up per config set — `GET /stats?config_set_id=...`. Each
config set can also have its own webhook destination so events for
Acme go to one URL and events for Globex go to another.
## Hard-bounce auto-list-cleaning
Hard bounces are added to your suppression list automatically. To
keep your application's email list in sync, listen for the
`Bounce` webhook event:
```js
app.post("/webhooks/splashify", async (req, res) => {
const sig = req.header("x-splashify-signature");
if (!verifyHMAC(req.rawBody, sig, process.env.SPLASHIFY_WEBHOOK_SECRET)) {
return res.status(401).end();
}
if (req.body.eventType === "Bounce" && req.body.bounce.bounceType === "Permanent") {
const email = req.body.bounce.bouncedRecipients[0].emailAddress;
await db.users.updateOne({ email }, { $set: { emailBouncedHard: true } });
}
res.status(200).end();
});
```
Now your signup form / re-engagement campaigns can skip these
addresses up-front instead of burning send quota.
## Retry on 5xx
Network blips and brief upstream issues should retry; auth/quota
errors shouldn't. Pseudocode:
```js
async function sendWithRetry(payload, attempts = 3) {
for (let i = 0; i < attempts; i++) {
const res = await client.emails.send(payload);
if (res.success) return res;
const code = res.error;
// Don't retry these — won't get better with time.
if (["INVALID_REQUEST", "FROM_NOT_VERIFIED", "INSUFFICIENT_BALANCE", "MISSING_AUTH"].includes(code)) {
throw new Error(`${code}: ${res.message}`);
}
// Backoff on the rest.
await sleep(1000 * Math.pow(2, i));
}
throw new Error("send_failed_after_retries");
}
```
## Webhooks that survive your deploys
Stamp every event in your local DB before processing — duplicate
webhook deliveries (which happen on retries) are idempotent:
```js
const eventID = req.header("x-splashify-delivery-id");
const inserted = await db.events.insertOne({
_id: eventID,
...req.body,
}, { ignoreDuplicates: true });
if (!inserted.insertedCount) return res.status(200).end(); // dedup
// ... process
```
`X-Splashify-Delivery-ID` is a fresh UUID per delivery attempt —
even a retried event keeps the same ID.
## Cold-start IP warm-up
New production accounts inherit warm sending infrastructure with
established reputation across major mailbox providers. You don't
need to manually warm up.
Dedicated IPs (roadmap) require warm-up — typically 2 weeks of
gradually increasing volume. We'll publish a warm-up calculator
when dedicated IPs ship.
========================================================================
## SDKs
URL: https://partner-docs.splashifypro.com/build-with-ai/sdks
========================================================================
# SDKs
Official SDKs ship for **Node.js**, **Python**, and **PHP**. All
three are auto-generated from the same OpenAPI spec the API runs
against, so they stay in lock-step with the platform.
| Language | Package | Source |
|---|---|---|
| Node.js | `@splashifypro/sdk` | [github.com/splashifypro/sdk-node](https://github.com/splashifypro/sdk-node) |
| Python | `splashifypro` | [github.com/splashifypro/sdk-python](https://github.com/splashifypro/sdk-python) |
| PHP | `splashifypro/sdk` | [github.com/splashifypro/sdk-php](https://github.com/splashifypro/sdk-php) |
Don't see your language? The API is plain HTTP/JSON — every
endpoint can be called with the language's stdlib HTTP client. See
the per-language [Quickstarts](/getting-started/node-quickstart) for
hand-rolled examples.
## Node.js
```bash
npm install @splashifypro/sdk
```
```js
const client = new SplashifyClient({
apiKey: process.env.SPLASHIFY_API_KEY,
});
const result = await client.emails.send({
from: "hello@yourcompany.com",
to: ["customer@example.com"],
subject: "Welcome",
htmlBody: "Welcome
",
});
console.log(result.results[0].messageId);
```
TypeScript types ship with the package. Edge runtimes (Vercel,
Cloudflare Workers, Deno) are supported.
## Python
```bash
pip install splashifypro
```
```python
from splashifypro import SplashifyClient
client = SplashifyClient(api_key=os.environ["SPLASHIFY_API_KEY"])
result = client.emails.send(
from_="hello@yourcompany.com",
to=["customer@example.com"],
subject="Welcome",
html_body="Welcome
",
)
print(result["results"][0]["message_id"])
```
Async client also available:
```python
from splashifypro import AsyncSplashifyClient
async with AsyncSplashifyClient(api_key=...) as client:
result = await client.emails.send(...)
```
## PHP
```bash
composer require splashifypro/sdk
```
```php
use Splashifypro\Client;
$client = new Client(getenv('SPLASHIFY_API_KEY'));
$result = $client->emails->send([
'from' => 'hello@yourcompany.com',
'to' => ['customer@example.com'],
'subject' => 'Welcome',
'html_body' => 'Welcome
',
]);
echo $result['results'][0]['message_id'];
```
Laravel users can pull in our facade by registering the package's
service provider — see the README on
[github.com/splashifypro/sdk-php](https://github.com/splashifypro/sdk-php).
## Versioning
SDKs follow [semantic versioning](https://semver.org). Major version
bumps only happen for breaking changes; the API itself is versioned
under `/api/v1/` and we'll ship `/api/v2/` before any breaking
contract change so the SDK can support both.
## Reporting issues
Each SDK lives in its own repo and accepts issues + PRs:
- [github.com/splashifypro/sdk-node/issues](https://github.com/splashifypro/sdk-node/issues)
- [github.com/splashifypro/sdk-python/issues](https://github.com/splashifypro/sdk-python/issues)
- [github.com/splashifypro/sdk-php/issues](https://github.com/splashifypro/sdk-php/issues)
For platform-side bugs (the API misbehaving regardless of SDK), or
for anything you can't reduce to a single language, email
**support@splashifypro.in** with the request payload + the
`pk_live_` key prefix (first 12 chars).
========================================================================
## Knowledge Base
URL: https://partner-docs.splashifypro.com/knowledge-base
========================================================================
# Knowledge Base
Long-form guides + reference material that goes deeper than the
endpoint docs. Read these when you want to understand WHY the API
works the way it does — or when you're debugging deliverability.
## Topics
- [**Concepts**](/knowledge-base/concepts) — sending identities,
configuration sets, suppression lists, sandbox vs production,
reputation
- [**Deliverability**](/knowledge-base/deliverability) — SPF / DKIM
/ DMARC, IP warm-up, content best practices, sender reputation
- [**Pricing**](/knowledge-base/pricing) — flat ₹0.01 per email,
sandbox free tier, billing model
- [**Errors**](/knowledge-base/errors) — every error code, what it
means, how to fix it
- [**FAQ**](/knowledge-base/faq) — common questions
## Quick orientation
If you're new, read these in order:
1. [Sending identities](/knowledge-base/concepts/sending-identities)
2. [Configuration sets](/knowledge-base/concepts/configuration-sets)
3. [Sandbox vs production](/knowledge-base/concepts/sandbox)
4. [SPF / DKIM / DMARC](/knowledge-base/deliverability/spf-dkim-dmarc)
5. [Pricing](/knowledge-base/pricing)
========================================================================
## Concepts
URL: https://partner-docs.splashifypro.com/knowledge-base/concepts
========================================================================
# Concepts
A short tour of the primitives the API exposes. If you've worked
with AWS SES the names map 1:1 — sending identities, configuration
sets, suppression lists, all here.
## The 5 primitives
```
┌─────────────────────────────────────────────────────────┐
│ Your Splashify Pro account │
│ │
│ ┌───────────────┐ ┌───────────────┐ ┌─────────────┐ │
│ │ Identity │ │ Identity │ │ Identity │ │
│ │ (your domain) │ │ (alt addr) │ │ (3rd domain)│ │
│ └───────┬───────┘ └───────┬───────┘ └──────┬──────┘ │
│ │ verified │ verified │ pending │
│ ┌───────┴──────────────────┴─────────────────┴──────┐ │
│ │ Configuration Sets │ │
│ │ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ "production" │ │ "marketing" │ │ │
│ │ │ ↓ events │ │ ↓ events │ │ │
│ │ │ → webhook A │ │ → webhook B │ │ │
│ │ │ → webhook B │ │ │ │ │
│ │ └──────────────┘ └──────────────┘ │ │
│ └────────────────────────────────────────────────────┘ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Suppression list (account-wide) │ │
│ │ bounced@example.com angry@example.com ... │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ Wallet: ₹X.XX Sandbox: ON / OFF │
└─────────────────────────────────────────────────────────┘
```
1. **[Sending identities](/knowledge-base/concepts/sending-identities)** —
verified domains + email addresses you can send `From:`.
2. **[Configuration sets](/knowledge-base/concepts/configuration-sets)** —
logical groupings of sends that route events to webhook
destinations.
3. **[Suppression list](/knowledge-base/concepts/suppression)** —
account-wide blocklist. Auto-populated by hard bounces +
complaints + unsubscribes.
4. **[Reputation](/knowledge-base/concepts/reputation)** — your
account's bounce + complaint rate. Drives auto-pausing if
thresholds breach.
5. **[Sandbox vs production](/knowledge-base/concepts/sandbox)** —
sandbox = 200/day cap + verified-only recipients. Production =
real volume after a quick review.
## Hierarchy
- **Identity** is account-wide. Verifying `acme.com` lets you send
from any address at `acme.com`.
- **Configuration set** scopes sends — events from sends inside
a config set go to that set's webhook destinations.
- **Suppression list** is account-wide by default. Each config set
can opt to also suppress per-config-set on bounces / complaints
via `suppression_options`.
## Send-time flow
```
POST /send (with config_set + from + to)
│
▼
1. From-domain on a verified identity? ──── no → 400 FROM_NOT_VERIFIED
│ yes
▼
2. Recipient on suppression list? ──── yes → status=rejected
│ no
▼
3. Wallet balance >= ₹0.01? ──── no → 402 INSUFFICIENT_BALANCE
│ yes (or in sandbox free tier)
▼
4. Daily quota not exceeded? ──── no → 429 SANDBOX_QUOTA_REACHED
│ yes
▼
5. Queue the email for delivery
│
▼
6. Fire "Send" event → webhook destinations
│
▼
7. SMTP attempt → recipient MX
├── 250 OK → "Delivery" event
├── 5xx hard bounce → "Bounce" event + suppress
└── 4xx soft bounce → retry (5min, 30min, 2h)
```
========================================================================
## Configuration Sets
URL: https://partner-docs.splashifypro.com/knowledge-base/concepts/configuration-sets
========================================================================
# Configuration Sets
A **configuration set** is a logical bundle of sends that share
event-destination wiring + suppression behaviour. Modelled directly
on AWS SES configuration sets.
If you're shipping email for a single product, you might never
create a config set — defaults work fine. But the moment you have:
- **Multiple downstream tenants** (one for each customer)
- **Multiple email types** (transactional vs marketing)
- **Distinct webhook destinations** per use case
...config sets become essential.
## What a config set carries
| Field | Purpose |
|---|---|
| `name` | Human-friendly identifier you reference on `/send` |
| `description` | Free-text notes |
| `customer_id` | Optional — partner's downstream end-customer attribution |
| `sending_enabled` | Kill switch — disable sends through this set without deleting it |
| `reputation_tracking_enabled` | Whether bounces/complaints from this set count toward your reputation rolling window |
| `suppression_options` | Which categories auto-add to suppression list: `NONE` / `BOUNCE` / `COMPLAINT` / `BOUNCE_AND_COMPLAINT` |
| `tags` | Arbitrary key-value pairs that ship in the webhook `mail.tags` field |
## Create one
```bash
curl https://api.splashifypro.com/api/v1/partner/email/configuration-sets \
-H "Authorization: Bearer $SPLASHIFY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "production",
"description": "Production transactional sends",
"suppression_options": "BOUNCE_AND_COMPLAINT",
"reputation_tracking_enabled": true,
"tags": {
"env": "prod",
"team": "platform"
}
}'
```
## Reference on send
```json
{
"from": "alerts@yourcompany.com",
"to": ["customer@example.com"],
"subject": "Payment received",
"html_body": "...",
"configuration_set_name": "production"
}
```
Events from this send fan out to every event destination attached
to the `production` config set.
## Multi-tenant pattern
If you're running a SaaS that sends email on behalf of customers,
create one config set per customer:
```js
// On customer signup:
const cfg = await client.configurationSets.create({
name: `customer_${customer.id}`,
customer_id: customer.id,
description: `Email sends for ${customer.name}`,
tags: { customer_id: customer.id, plan: customer.plan },
});
// On send:
await client.emails.send({
from: customer.fromAddress,
to: [recipient],
configuration_set_name: `customer_${customer.id}`,
...
});
```
Now per-customer stats roll up cleanly via:
```bash
curl 'https://api.splashifypro.com/api/v1/partner/email/stats?config_set_id=...' \
-H "Authorization: Bearer $SPLASHIFY_API_KEY"
```
And per-customer event destinations let each customer have their
own webhook URL.
## Event destinations
A config set without event destinations still records events
internally — they show up in `GET /events` and `GET /stats` — but
nothing fans out to your servers.
Add a webhook destination:
```bash
curl https://api.splashifypro.com/api/v1/partner/email/configuration-sets/$CFG/event-destinations \
-H "Authorization: Bearer $SPLASHIFY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "production-webhook",
"destination_type": "WEBHOOK",
"webhook_url": "https://yourapp.com/webhooks/email",
"webhook_secret": "...",
"matching_event_types": ["send", "delivered", "bounce", "complaint", "open", "click"]
}'
```
Multiple destinations per set are fine — useful for routing
engagement events to one URL and deliverability events to another.
## Disabling a config set
```bash
curl -X PATCH https://api.splashifypro.com/api/v1/partner/email/configuration-sets/$CFG \
-H "Authorization: Bearer $SPLASHIFY_API_KEY" \
-d '{"sending_enabled": false}'
```
Sends with `configuration_set_name` matching this set get
`status: rejected` until re-enabled. In-flight sends complete.
## Suppression options
| Value | Behaviour |
|---|---|
| `NONE` | Auto-suppression OFF for this set's sends |
| `BOUNCE` | Hard bounces auto-add to suppression list |
| `COMPLAINT` | Complaints auto-add to suppression list |
| `BOUNCE_AND_COMPLAINT` | Both auto-add (default + recommended) |
Manual suppressions via `PUT /suppression/:email` always go to the
account-wide list regardless of config-set settings.
## Reputation tracking
When `reputation_tracking_enabled: false`, bounces + complaints
from this set's sends are excluded from your account's rolling
reputation calculation. Useful for:
- Test campaigns where you knowingly send to low-quality addresses
- Risk-isolated experiments
Defaults to `true`. Don't turn this off unless you really mean it.
## Limits
- 100 configuration sets per partner account
- 5 event destinations per configuration set
- Names are unique per partner, case-sensitive, alphanumeric +
`_` + `-`, 1-256 chars
========================================================================
## Reputation
URL: https://partner-docs.splashifypro.com/knowledge-base/concepts/reputation
========================================================================
# Reputation
Mailbox providers (Gmail, Outlook, Yahoo, Apple Mail) decide
whether to deliver your mail to inbox, junk, or refuse it
entirely based on **sender reputation** — a continuously-updated
score derived from your bounce rate, complaint rate, content
patterns, and recipient engagement.
We track two of the load-bearing signals:
- **Bounce rate** — `bounced / sent` over the rolling 14-day window
- **Complaint rate** — `complained / sent` over the rolling 14-day window
Both are computed from the per-day counter table behind
`GET /partner/email/stats`.
## Status thresholds
```
bounce > 10% OR complaint > 0.5% → PAUSED (auto-pause)
bounce > 5% OR complaint > 0.1% → AT_RISK (warning)
else → HEALTHY
```
These match AWS SES and the broader industry-standard
"watch-list" thresholds.
## What happens at each threshold
### `HEALTHY`
Default state. No special handling.
### `AT_RISK`
- Email notification to the partner contact
- Banner on the partner panel dashboard
- Recommendation to investigate recent campaigns + clean lists
- No automatic action against your sending — but you should fix the
underlying issue immediately
### `PAUSED`
- All `/send` calls return `403 SENDING_PAUSED`
- The reputation status reflects on `GET /partner/email/quotas`
- Your panel dashboard shows the alert + reason
- An admin reviews + decides whether to:
- Drop you back to sandbox (so you can test fixes)
- Re-enable with a stern warning
- Keep paused pending list-cleaning evidence
To resume sending after PAUSED:
1. Identify the breach source (which campaigns / list segments
caused it)
2. Clean the list — remove every address that bounced / complained
3. Reach out to support with your remediation plan
4. Admin can lift the pause via the partner panel
## What we don't track (yet)
- **Engagement signals** (open rate, reply rate, forward rate) —
on roadmap for shared-IP reputation; today these only matter for
dedicated-IP customers.
- **Spam-filter scores** (SpamAssassin etc.) — you control content;
our infrastructure handles authentication.
- **Domain age / DMARC alignment** — hardened automatically through
the verification flow.
## Per-config-set vs account-wide
By default reputation is computed at the **account level**. Sends
across all config sets contribute to the same rolling window.
If you want to isolate a high-risk experiment from your main
reputation, set `reputation_tracking_enabled: false` on that
config set. Bounces + complaints from those sends are still
recorded in your event log + suppression list, but excluded from
the rolling reputation calculation.
Use sparingly — `false` is an opt-out from a healthy default.
## Inspecting your reputation
```bash
curl https://api.splashifypro.com/api/v1/partner/email/reputation \
-H "Authorization: Bearer $SPLASHIFY_API_KEY"
```
Response:
```json
{
"success": true,
"window_days": 14,
"sent": 12500,
"bounced": 218,
"complained": 4,
"bounce_rate": 0.01744,
"complaint_rate": 0.00032,
"status": "HEALTHY"
}
```
`/quotas` returns the same status alongside daily-quota info — use
that endpoint as the single read-once-per-page source on your
dashboard.
## How to keep reputation healthy
1. **Send to opt-in lists only.** Buying lists or scraping email
addresses is the #1 cause of complaint-rate breaches.
2. **Honor unsubscribes within 10 days.** It's a CAN-SPAM /
CASL / DPDP requirement AND it's how you avoid complaint
spikes.
3. **Verify before sending.** Hitting hundreds of stale addresses
once spikes your bounce rate hard.
4. **Warm up new identities slowly.** Mailbox providers throttle
first-contact volume — start with low volume to high-engagement
recipients, ramp over a week or two.
5. **Watch your DMARC reports.** Misaligned mail (sent through us
but with wrong From-domain) gets quarantined or rejected — that
shows up as bounces in our stats.
========================================================================
## Sandbox vs Production
URL: https://partner-docs.splashifypro.com/knowledge-base/concepts/sandbox
========================================================================
# Sandbox vs Production
Every new partner account starts in **sandbox mode**. Sandbox is a
deliberately constrained version of production where:
| Limit | Sandbox | Production (default) |
|---|---|---|
| Daily send quota | **200 emails/day** | 50,000 emails/day |
| Peak send rate | **1 email/second** | 14 emails/second |
| Recipient restrictions | Verified-recipient addresses only (TODO; today: cap-only) | Anyone |
| Per-email price | First 200/day **free** | ₹0.01/email from email #1 |
| All other features | Identical to production | — |
Sandbox is NOT a free trial. It's a probation period — we want to
see you can verify a domain, configure webhooks, and send a few
test emails without making the platform's shared IP reputation
worse.
## Why we ship sandbox by default
In our first year of operation, ~30% of new accounts caused some
form of deliverability damage in week 1. Reasons:
- Buying / scraping lists
- Testing in production with bogus recipients
- Misconfigured templates that 100% bounced
Sandbox protects everyone. Once you've shown a clean signal — even
just a handful of successful sends + a verified domain — production
access is fast.
## What still works in sandbox
Everything at the API level. You can:
- Verify domain identities + email-address identities
- Create configuration sets + event destinations + templates
- Send up to 200/day to whoever
- Receive webhooks normally
- Add suppression entries + check stats + check reputation
The constraints are **velocity-only** — daily quota + rate limit.
The functional surface is identical.
## Requesting production access
```bash
curl https://api.splashifypro.com/api/v1/partner/email/production-access \
-H "Authorization: Bearer $SPLASHIFY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"use_case": "We send transactional emails for our SaaS app — signup confirmation, password reset, payment receipts, weekly digest. ~5000 emails/day across all customers.",
"email_volume_estimate": "5000-15000 per day",
"has_unsubscribe_method": true,
"has_consent_proof": true
}'
```
Required fields:
| Field | What we want |
|---|---|
| `use_case` | A real description (≥30 chars). Says WHO you're sending to + WHY they signed up |
| `email_volume_estimate` | Realistic daily volume. Don't overstate; quotas can be raised later |
| `has_unsubscribe_method` | Confirm you have an unsubscribe link in marketing emails |
| `has_consent_proof` | Confirm recipients opted in (signup, double-opt-in, purchase, etc.) |
All four must be present. Faking them violates our AUP — we audit
periodically + downgrade accounts that lied.
## Review timeline
Most requests are reviewed within **24 business hours**. If your
request:
- Has a clear use case + verified domain + zero suppression-list
entries → typically approved within an hour
- Has a vague use case OR no verified domain yet → comes back with
questions
- Looks like list-buying / cold outreach → denied with reason
You can resubmit any number of times after a denial — fix the
flagged issue + try again.
## What approval changes
```
sandbox: true → sandbox: false
daily_send_quota: 200 → 50,000
peak_send_rate_per_second: 1 → 14
```
These are baseline production numbers. For higher volume reach
out to support — caps can be raised on a per-partner basis.
## Inspecting your status
```bash
curl https://api.splashifypro.com/api/v1/partner/email/quotas \
-H "Authorization: Bearer $SPLASHIFY_API_KEY"
```
Returns:
```json
{
"daily_send_quota": 200,
"peak_send_rate_per_second": 1,
"sandbox": true,
"sent_today": 47,
"sandbox_free_used_today": 47,
"reputation_status": "HEALTHY",
...
}
```
## Common denial reasons
- **No use case description.** Empty + 1-line submissions are
declined unread.
- **No verified domain.** We require at least one domain identity
in `VERIFIED` status before approving. Sandbox is a 5-minute
exercise — verify one + resubmit.
- **Vague description.** "Sending email to our users" isn't enough.
Tell us WHAT kind of email + HOW recipients opted in.
- **Unsubscribe gap.** Marketing email without an unsubscribe link
is illegal under CAN-SPAM, CASL, DPDP. Fix it before requesting.
- **Cold-list pattern.** "We bought a list of 100K emails" is a
decline.
========================================================================
## Sending Identities
URL: https://partner-docs.splashifypro.com/knowledge-base/concepts/sending-identities
========================================================================
# Sending Identities
A **sending identity** is anything you've proven you control:
- A **domain** (preferred — covers any address at that domain), or
- A specific **email address** (limited to that one mailbox)
Every email you send must have a `From:` address that matches a
**verified** identity. Sending from an unverified address returns
`400 FROM_NOT_VERIFIED`.
## Why verification?
Without verification, anyone could claim to send from
`security@yourbank.com` through our infrastructure. Verification
proves you control the domain or address at the DNS / mailbox level.
This is the same reason AWS SES, SendGrid, Postmark, Mailgun, and
every other ESP requires it. It's not optional in 2026's email
landscape.
## Domain verification
Verifying a domain takes 3 DNS records. Once published + checked,
you can send from **any address** at the domain — `hello@`,
`alerts@`, `noreply@`, etc.
Create the identity:
```bash
curl https://api.splashifypro.com/api/v1/partner/email/identities \
-H "Authorization: Bearer $SPLASHIFY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"identity_type": "DOMAIN", "identity_value": "yourcompany.com"}'
```
The response gives you the 3 records to publish:
| Type | Hostname | Value |
|---|---|---|
| TXT | `yourcompany.com` | `v=spf1 include:_spf.mail.splashifypro.com ~all` |
| CNAME | `splashify._domainkey.yourcompany.com` | `splashify._domainkey.mail.splashifypro.com` |
| TXT | `_dmarc.yourcompany.com` | `v=DMARC1; p=quarantine; rua=mailto:dmarc@splashifypro.com` |
After publishing on your DNS provider, trigger a re-check:
```bash
curl -X POST https://api.splashifypro.com/api/v1/partner/email/identities/DOMAIN/yourcompany.com/verify \
-H "Authorization: Bearer $SPLASHIFY_API_KEY"
```
Once `"status": "VERIFIED"` lands, you're good. We re-check
verified domains every 24 hours; if any record disappears, status
flips back to `PENDING` and we email you.
## Email-address verification
For low-volume use cases or when you can't add DNS records (you're
using a public-domain mailbox like Gmail), verify a single address:
```bash
curl https://api.splashifypro.com/api/v1/partner/email/identities \
-H "Authorization: Bearer $SPLASHIFY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"identity_type": "EMAIL_ADDRESS", "identity_value": "alerts@yourcompany.com"}'
```
We email a verification link to the address. Click it (or use the
verify endpoint directly with the included token) and the address
becomes verified.
> **Public-domain caveat:** We refuse to verify addresses at common
> public providers (`gmail.com`, `yahoo.com`, `outlook.com`, etc.)
> — sending bulk through `you@gmail.com` violates Google's TOS and
> would get our IP blocklisted within hours. Use your own domain.
## Display name vs verified address
You can set a friendly display name without verifying it:
```json
{
"from": "Sarah from Acme "
}
```
`acme.com` must be verified. `Sarah from Acme` is unverified —
recipients see whatever string you supply.
## Multiple domains
Verify as many domains as you like. Common pattern: one for
transactional (`mail.acme.com`), one for marketing (`news.acme.com`).
Helps deliverability — bounces / complaints on the marketing domain
don't drag down transactional reputation.
## Listing your identities
```bash
curl https://api.splashifypro.com/api/v1/partner/email/identities \
-H "Authorization: Bearer $SPLASHIFY_API_KEY"
```
## Removing an identity
```bash
curl -X DELETE https://api.splashifypro.com/api/v1/partner/email/identities/DOMAIN/yourcompany.com \
-H "Authorization: Bearer $SPLASHIFY_API_KEY"
```
In-flight sends from that domain complete normally; new sends are
rejected with `FROM_NOT_VERIFIED`.
## Auto-recheck behaviour
Verified domains: re-checked every 24 hours.
Pending domains: re-checked every 1 hour for 7 days, then drop to
`FAILED` if all three records still aren't published.
You can always re-trigger via `POST /verify`.
## Read more
- [SPF / DKIM / DMARC explainer](/knowledge-base/deliverability/spf-dkim-dmarc)
- [API reference: identities](/api-reference/identities/create)
========================================================================
## Suppression List
URL: https://partner-docs.splashifypro.com/knowledge-base/concepts/suppression
========================================================================
# Suppression List
The suppression list is your account's blocklist of recipient
addresses we won't send to. It exists to protect:
- **Your reputation** — repeated bounces / complaints tank your
bounce + complaint rate, which mailbox providers use as a
primary signal.
- **The recipient** — opting out, manually unsubscribing, or
marking as spam should mean they never hear from you again.
- **The platform** — collectively low bounce/complaint rates keep
the platform's deliverability healthy across all senders.
## What gets auto-added
| Trigger | Reason |
|---|---|
| Hard bounce | Permanent delivery failure (no such user, mailbox terminated, etc.) |
| Complaint | Recipient marked the email as spam (FBL report from inbox provider) |
| Unsubscribe | Recipient clicked a one-click unsubscribe (RFC 8058) link |
Soft bounces (mailbox full, server temporarily unavailable, etc.)
do NOT auto-suppress — we retry up to 3 times, then mark the row
failed without suppressing.
## What we DON'T auto-add
- Single soft bounces (we retry first)
- API rejections (e.g. invalid recipient format) — those don't
reach SMTP
- Greylist 4xx responses (we retry per-domain backoff)
## Behaviour at send time
When you call `/send`, every recipient is checked against the
suppression list **before** any SMTP attempt. Recipients on the
list:
- Get `status: rejected` in the per-recipient response
- Don't burn wallet balance — you're not billed for rejected
recipients
- Generate a `Reject` webhook event with `reason: "suppressed"`
This matters at scale — sending a bulk to a list with 5%
suppressed addresses saves you 5% of the wallet hit + keeps your
bounce rate clean.
## Listing the suppression list
```bash
curl 'https://api.splashifypro.com/api/v1/partner/email/suppression?reason=BOUNCE&limit=100' \
-H "Authorization: Bearer $SPLASHIFY_API_KEY"
```
Filters: `reason` (BOUNCE / COMPLAINT / UNSUBSCRIBE / MANUAL),
`search` (substring), `limit` (1-1000).
## Adding manually
```bash
curl -X PUT https://api.splashifypro.com/api/v1/partner/email/suppression/legal-hold@example.com \
-H "Authorization: Bearer $SPLASHIFY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"reason": "MANUAL", "details": "GDPR erasure request 2026-05-03"}'
```
Use cases:
- GDPR / DPDP erasure requests
- Customer-side opt-outs that didn't come through your unsubscribe
flow
- Known-bad addresses you want to skip preemptively
## Removing
```bash
curl -X DELETE https://api.splashifypro.com/api/v1/partner/email/suppression/customer@example.com \
-H "Authorization: Bearer $SPLASHIFY_API_KEY"
```
Removal lets you re-send to that address. Be conservative — if the
address bounced hard or complained, removing it and re-sending is
likely to get you suppressed again + drag your reputation down.
## Per-config-set suppression scope
By default, suppressions are account-wide. Each config set can
override which categories auto-suppress via the `suppression_options`
field:
| Value | What gets auto-suppressed |
|---|---|
| `NONE` | Nothing (rare — you're saying "send everything regardless") |
| `BOUNCE` | Hard bounces only |
| `COMPLAINT` | Complaints only |
| `BOUNCE_AND_COMPLAINT` | Both (recommended default) |
The check at send time still hits the account-wide list — config-
set settings only affect what's auto-WRITTEN to the list.
## Best practices
- **Don't programmatically remove suppressions in bulk.** Each
removed entry that re-bounces hurts you twice.
- **Sync to your application's user table.** Listen to `Bounce`
+ `Complaint` webhooks, mark those users `email_unsubscribed: true`
in your DB, and skip them in your own outbound logic. Defense in
depth.
- **Honor unsubscribe requests instantly.** If a customer clicks
unsubscribe in your app's preferences page, push their email to
our suppression list via PUT — don't wait for the next campaign
to filter them.
- **Audit periodically.** Run `GET /suppression?reason=BOUNCE` once
a month and cross-reference against your active customer list —
bounced addresses on your billing roster mean broken support
delivery + missed renewal emails.
========================================================================
## Deliverability
URL: https://partner-docs.splashifypro.com/knowledge-base/deliverability
========================================================================
# Deliverability
Sending email isn't enough. **Inboxing** is.
Mailbox providers (Gmail, Outlook, Yahoo, Apple Mail, etc.) decide
whether your message lands in inbox, junk, or gets refused based on
a stack of signals — domain authentication, sender reputation,
content patterns, recipient engagement.
This page is the field guide.
## The 90% rule
Most deliverability problems come from one of three causes:
1. **Authentication is broken** — SPF/DKIM/DMARC misconfigured →
provider can't verify the sender → marks as spam.
2. **List quality is bad** — too many bounces (sending to dead
addresses) or too many complaints (recipients didn't expect
the email).
3. **Content tripped a spam filter** — links in the body to
blocklisted domains, misleading subject lines, missing
plaintext alternative.
Get authentication right + send to opt-in lists + don't write spammy
copy and you'll inbox.
## SPF / DKIM / DMARC
The three DNS-level authentication mechanisms every modern inbox
provider expects.
### SPF (Sender Policy Framework)
A TXT record on your domain that lists IPs / providers authorized
to send mail "from" your domain. We give you:
```
TXT yourcompany.com "v=spf1 include:_spf.mail.splashifypro.com ~all"
```
The `include:` mechanism delegates SPF lookup to our published
record, so when our IPs change you don't have to update yours.
### DKIM (DomainKeys Identified Mail)
A cryptographic signature in the email header that lets the
receiver verify the message wasn't tampered with in transit. We
publish the public key under our domain and you publish a CNAME
that points at it:
```
CNAME splashify._domainkey.yourcompany.com splashify._domainkey.mail.splashifypro.com
```
This way you don't manage private keys + we can rotate the key
periodically without coordinating with every customer.
### DMARC (Domain-based Message Authentication, Reporting & Conformance)
DMARC sits on top of SPF + DKIM. It tells the receiver what to do
with unauthenticated mail claiming to be from your domain:
```
TXT _dmarc.yourcompany.com "v=DMARC1; p=quarantine; rua=mailto:dmarc@splashifypro.com"
```
| Policy | Behaviour |
|---|---|
| `p=none` | Monitor only — receivers report but don't act |
| `p=quarantine` | Send to spam (recommended) |
| `p=reject` | Refuse outright (strongest, but risky if any legitimate sender isn't authenticated) |
The `rua=` address receives aggregate reports. We provide a public
endpoint at `dmarc@splashifypro.com` so you don't need to set up
your own.
### Verify all three pass
```bash
curl https://api.splashifypro.com/api/v1/partner/email/identities/DOMAIN/yourcompany.com \
-H "Authorization: Bearer $SPLASHIFY_API_KEY"
```
Response includes per-record `pass: true/false`:
```json
{
"dns_records": {
"spf": { "pass": true, "found": "v=spf1 include:_spf.mail.splashifypro.com ~all" },
"dkim": { "pass": true, "found": "CNAME splashify._domainkey.mail.splashifypro.com" },
"dmarc": { "pass": true, "found": "v=DMARC1; p=quarantine; rua=mailto:..." }
}
}
```
## List hygiene
Bounces and complaints are the #2 cause of deliverability decline.
### Acquire opt-in addresses only
Single opt-in (email entered on a form, no confirmation step) is
the minimum. Double opt-in (form + confirmation email + click
the link) is the gold standard — drops bounce rates ~10x.
Don't:
- Buy lists
- Scrape websites
- Import a list of "all my contacts" from a co-founder's old job
- Email people who gave you their card at a conference (without
saying "we'll add you to our newsletter")
### Bounce + complaint handling
Both are auto-managed by the [suppression list](/knowledge-base/concepts/suppression).
Hard bounces and complaints get added immediately, and we never
attempt to send to suppressed addresses.
Sync to your application:
- `Bounce` webhook event → mark user `email_bounced=true` in your DB
- `Complaint` webhook event → mark user `email_complained=true` in your DB
Skip these users from any future outbound — your suppression list
is mirrored at our level, but having it locally lets you also skip
them from marketing campaigns + product onboarding flows.
### Engagement-based pruning
Mailbox providers weight engagement heavily. If a recipient hasn't
opened or clicked any of your emails in 6 months, sending to them
hurts your reputation. Drop them from active campaigns until they
re-engage (e.g. via a "we miss you" prompt).
## Content best practices
| Do | Don't |
|---|---|
| Plain `From:` (`hello@yourcompany.com`) | Display-name-only fakery (`Hello `) |
| Clear subject (no `RE:` / `FWD:` if not actually a reply) | All-caps, all-emojis, money symbols |
| Both HTML + plaintext body | HTML only — spam filters penalize |
| Unsubscribe link in marketing email | Marketing email without unsubscribe (illegal in most jurisdictions) |
| Image alt text | Image-only emails (heavy spam signal) |
| Concise body | 50% link / 50% text ratio |
We auto-generate plaintext alternative from your HTML if you don't
provide one. We also auto-inject the unsubscribe link if your
template doesn't include one (CAN-SPAM compliance).
## Sender reputation
We track [reputation](/knowledge-base/concepts/reputation) at the
account level. Bounce rate + complaint rate over rolling 14 days.
Above-threshold rates trigger automatic warnings + eventual
auto-pause.
The platform's sending infrastructure is well-warmed and has good
standing across major mailbox providers. You inherit that
reputation when you start sending — but bad behaviour from your
account hurts the broader platform reputation, which is why we're
strict about list quality.
## Subdomain strategy
For organisations sending high volume of mixed mail types, use
subdomains to isolate reputation:
| Domain | Use |
|---|---|
| `mail.acme.com` | Transactional (signup, receipts, password reset) |
| `news.acme.com` | Marketing campaigns / newsletters |
| `notify.acme.com` | App notifications (high frequency, low engagement) |
Each gets its own verified identity. Bounces / complaints on
`news.acme.com` don't drag down `mail.acme.com`'s reputation.
## Read more
- [SPF / DKIM / DMARC explainer](/knowledge-base/concepts/sending-identities)
- [Reputation thresholds + auto-pause](/knowledge-base/concepts/reputation)
- [Suppression list mechanics](/knowledge-base/concepts/suppression)
========================================================================
## Errors
URL: https://partner-docs.splashifypro.com/knowledge-base/errors
========================================================================
# Errors
Every error from the Email API has a stable `error` code + a
human-readable `message`. Build retry / error-handling logic against
the code, not the message.
## Response shape
```json
{
"success": false,
"error": "INSUFFICIENT_BALANCE",
"message": "Wallet balance ₹0.00 — recharge to continue sending."
}
```
`success: false` is always present. `error` is the stable code.
`message` is for display + may change for clarity over time.
## 400 Bad Request
| Code | Cause | Fix |
|---|---|---|
| `INVALID_REQUEST` | Malformed JSON or missing required field | Read `message` |
| `INVALID_AMOUNT` | Non-positive amount on a wallet recharge | Pass amount > 0 |
| `INVALID_PARAM` | Path / query param doesn't parse | Check the URL |
| `MISSING_PARAM` | Required param missing | Read `message` |
| `FROM_NOT_VERIFIED` | `from` address not on a verified identity | Verify domain via `POST /identities` |
| `BILLING_INCOMPLETE` | Partner profile missing required fields | Fill `PUT /partner/self/billing` |
| `BILLING_NOT_SET_UP` | Zoho customer not yet created | Save billing once via `PUT /partner/self/billing` |
| `INVALID_SIGNATURE` | HMAC check on payment-verify failed | Don't tamper with payment widget output |
| `BAD_RECIPIENT` | Recipient address malformed | Validate before send |
| `TOO_MANY_RECIPIENTS` | Per-message recipient cap exceeded (50) | Split into multiple sends |
## 401 Unauthorized
| Code | Cause | Fix |
|---|---|---|
| `MISSING_AUTH` | No `Authorization` header | Add `Authorization: Bearer pk_live_...` |
| `INVALID_KEY_FORMAT` | Key doesn't match `pk_live_...` | Regenerate from partner panel |
| `KEY_NOT_FOUND` | Key was revoked or never existed | Regenerate |
| `KEY_INACTIVE` | Account suspended | Contact support |
## 402 Payment Required
| Code | Cause | Fix |
|---|---|---|
| `INSUFFICIENT_BALANCE` | Wallet balance < send price | Recharge wallet |
## 403 Forbidden
| Code | Cause | Fix |
|---|---|---|
| `IP_BLOCKED` | Your IP isn't on your account's IP allowlist | Add to allowlist or call from an allowed IP |
| `SENDING_PAUSED` | Account paused (admin OR reputation auto-pause) | Contact support / clean lists |
| `SANDBOX_QUOTA_REACHED` | Daily 200/day sandbox cap hit | Request production access |
| `FEATURE_LOCKED` | Plan doesn't include this feature | Upgrade plan |
## 404 Not Found
| Code | Cause | Fix |
|---|---|---|
| `PARTNER_NOT_FOUND` | Partner ID doesn't exist | Check the ID |
| `IDENTITY_NOT_FOUND` | Identity hasn't been created | Create via `POST /identities` |
| `CONFIG_SET_NOT_FOUND` | Configuration set name unknown | Check name spelling |
| `TEMPLATE_NOT_FOUND` | Template name not registered | Create via `POST /templates` |
| `MESSAGE_NOT_FOUND` | message_id doesn't match any send | Check the ID + day_bucket |
## 409 Conflict
| Code | Cause | Fix |
|---|---|---|
| `EMAIL_ALREADY_REGISTERED` | Signup with already-used email | Use forgot-password to recover |
| `IDENTITY_EXISTS` | Already verified — duplicate POST | Idempotent — existing row returned |
| `CONFIG_SET_NAME_TAKEN` | Another set with that name exists | Pick a unique name |
## 429 Too Many Requests
| Code | Cause | Fix |
|---|---|---|
| `RATE_LIMITED` | Per-second send rate exceeded | Backoff and retry |
| `OTP_RATE_LIMITED` | OTP resend before cooldown | Wait 60 seconds |
## 500 Internal Server Error
| Code | Cause | Fix |
|---|---|---|
| `DB_ERROR` | Database query failed | Retry with backoff — usually transient |
| `INTERNAL_ERROR` | Catch-all | Retry once; if persistent, contact support |
| `PAYMENT_GATEWAY_ERROR` | Zoho upstream returned 5xx | Retry — usually transient |
## 502 Bad Gateway
| Code | Cause | Fix |
|---|---|---|
| `PAYMENT_GATEWAY_ERROR` | Zoho returned an error response | Wait + retry; check Zoho status |
## 503 Service Unavailable
| Code | Cause | Fix |
|---|---|---|
| `DB_UNAVAILABLE` | Database connection issue | Retry — should clear within seconds |
## SMTP errors (in webhook payloads)
When a `Bounce` event fires, the `bounce.bouncedRecipients[].diagnosticCode`
field carries the SMTP diagnostic from the recipient's MX. Common ones:
| Code | Meaning |
|---|---|
| `550 5.1.1 user unknown` | Recipient doesn't exist (hard bounce) |
| `550 5.1.10 No such user` | Same — different MX wording |
| `550 5.7.1 spam policy` | Receiver classified as spam (treat as complaint) |
| `552 5.2.2 mailbox full` | Soft bounce — retry |
| `421 4.7.0 try again later` | Greylisting / load — retry |
| `554 5.7.1 sender rejected` | Sender domain blocked by recipient — list cleaning needed |
## Retry strategy
Rule of thumb:
- **400-class errors:** don't retry. Fix the request.
- **402:** don't retry. Recharge first.
- **403 + 404:** don't retry. Fix configuration first.
- **429:** retry with exponential backoff (start 1s, double up to 60s).
- **500 / 502 / 503:** retry up to 3 times with exponential backoff.
Idempotency: `/send` is NOT idempotent on retry — duplicate calls
with the same body will produce duplicate email sends. Implement
your own dedup against your business identifier (charge_id, etc.)
before calling `/send`.
## When to contact support
- 500-class errors persist > 5 minutes
- 402 even though wallet balance shows positive
- 403 SENDING_PAUSED with no clear cause in `/reputation`
Email support@splashifypro.in or open a ticket from the partner
panel.
========================================================================
## FAQ
URL: https://partner-docs.splashifypro.com/knowledge-base/faq
========================================================================
# FAQ
## Account & Billing
### How long does production access take?
Most requests are reviewed within 24 business hours. Clean
applications (verified domain, real use case, no obvious red flags)
are typically approved within an hour.
### Can I have multiple API keys?
Yes — generate as many as you need from **Settings → API Keys**.
Common pattern: one per environment (staging/production), one per
service (web app / cron worker / etc). Revoke individually without
affecting the others.
### Is there a free tier?
Sandbox accounts get 200 emails/day free. After production access,
you pay ₹0.01/email from email #1. There's no monthly minimum or
commitment.
### How do I close my account?
Email support@splashifypro.in. Wallet balance can either be
transferred to a new account or refunded under exceptional
circumstances.
## Sending
### Can I send from a Gmail address?
No. We refuse to verify public-domain mailboxes (gmail.com,
yahoo.com, outlook.com, etc.) — sending bulk through them violates
the providers' TOS and would get our IP blocklisted. Use your own
domain.
### Can I send to anyone?
In **production**, yes. In **sandbox**, only to verified-recipient
addresses (email-address identities you've explicitly added).
### What's the maximum recipients per send?
50 per `/send` request. For larger lists, use `/send-bulk` (50
destinations × 50 recipients = 500 max per request) or a
campaign-style fan-out.
### What's the maximum email size?
10 MB total (HTML + text + attachments). The MIME-encoded message
counts toward this limit.
### Can I attach files?
Not via `/send` directly today. `/send-raw` accepts a complete MIME
message (base64-encoded) — you build the multipart structure
yourself. Attachment-aware send is on the roadmap.
### Are there inline images?
With `/send-raw` you control the entire MIME tree, including
inline `Content-ID:` references. With `/send` use externally-hosted
images (e.g. on a CDN) — most modern email clients strip `cid:`
references when forwarded anyway.
## Domain & Identity
### Why do I need to verify a domain?
Without verification, anyone could claim to send from your domain
through our infrastructure. Verification proves ownership and is
required by every modern ESP for the same reason.
### How long does DNS verification take?
After publishing the records, typically 5-15 minutes. Some DNS
providers cache aggressively (Cloudflare flattening, GoDaddy
24-hour TTL) — call `POST /verify` after publishing and check
`status`.
### Can I use a subdomain?
Yes — `mail.acme.com` is a valid identity. Common pattern:
- `mail.acme.com` for transactional sends
- `news.acme.com` for marketing
This isolates reputation between mail types.
### What if my DKIM CNAME is flattened by my DNS provider?
Cloudflare's CNAME flattening serves the resolved TXT record
directly. We accept either CNAME OR TXT for the DKIM check —
flattened-but-correct records pass.
## Webhooks
### How fast do webhooks fire after a send?
Send + Reject webhooks fire within ~1 second of the API response.
Delivery webhooks fire within ~5-30 seconds (the SMTP attempt time).
Bounce webhooks fire within seconds (sync hard bounces) or minutes
to hours (async DSN bounces).
### What if my webhook endpoint is down?
We retry on 5xx + timeout per the [retry schedule](/webhooks/retries)
(1min, 5min, 15min). After ~21 minutes total we give up. 4xx
responses (auth, schema mismatch) get NO retry — your URL is broken.
### Can I have multiple webhook destinations?
Yes — up to 5 per configuration set. Useful for routing engagement
events to one URL and deliverability events to another.
### How do I rotate the webhook secret?
PATCH the destination with a new `webhook_secret`. The API never
echoes the new value back, so save it locally before the next
event fires. Brief overlap window is your responsibility — accept
both old + new for ~30 seconds during rotation.
## Pricing
### Is the rate marketing-vs-transactional?
No — flat ₹0.01 per email regardless of category. Simpler for both
sides.
### Are SMS credits separate?
Yes — SMS is a separate product with its own billing. This API is
email-only.
### Do I get a tax invoice?
Yes. Every wallet recharge generates a Zoho Billing invoice with
GST (for India-based partners with GSTIN). Downloadable from the
partner panel.
## Compliance
### Is this CAN-SPAM compliant?
The API supports compliance — auto-injected Unsubscribe headers
(RFC 8058 one-click), suppression list, no anonymous sending. But
compliance is YOUR responsibility — you must ensure recipients
opted in, your unsubscribe link works, and your physical mailing
address appears in the body.
### GDPR / DPDP?
Same answer — we provide the tools (suppression list for erasure
requests, audit logs for data subject access requests). Partner is
the data controller; we're the processor.
### What about CASL (Canada)?
Same model. Express consent + clear identification + working
unsubscribe + 10-day honor window. We honor the platform-level
suppression list immediately, but you must process unsubscribe
clicks via webhook and update your own user state.
## Migration
### I'm coming from AWS SES — how compatible is the API?
Endpoint shapes and response field names map 1:1 in most cases.
The webhook event payload shape matches AWS SES SNS event
publishing exactly. SDKs feel similar (resource → action). Most
migrations are a base-URL swap + an auth header swap.
### From Resend / Postmark / SendGrid?
Similar shapes. The biggest difference is the [configuration set
+ event destination model](/knowledge-base/concepts/configuration-sets)
which mirrors AWS SES rather than the per-message metadata model
some providers use. If you used SES configuration sets, you're
home.
### Can I forward existing webhooks?
We don't accept incoming webhooks (we deliver them, we don't
receive). For event-source migration, set up a webhook destination
on a new config set and migrate sends to use it.
## Support
### Where do I get help?
- **Maya AI assistant** — every page has the ✨ button bottom-right
- **Search** (Ctrl/⌘+K) — every doc page is indexed
- **Email** — support@splashifypro.in
- **Partner panel** — built-in support ticket flow
### Is there a status page?
Yes — [status.splashifypro.com](https://status.splashifypro.com).
We post incidents within ~5 minutes of detection.
### Where's the changelog?
Linked from the partner panel footer. Subscribe to the RSS feed
to get notified of new endpoints + behaviour changes.
========================================================================
## Pricing
URL: https://partner-docs.splashifypro.com/knowledge-base/pricing
========================================================================
# Pricing
We keep it simple — one rate for everyone, sandbox free, prepaid
wallet, no monthly minimums.
## Headline
| Tier | Rate |
|---|---|
| **Sandbox (default for new accounts)** | First 200/day **free**, then ₹0.01/email |
| **Production** | ₹0.01/email from email #1 |
That's it. No marketing-vs-transactional split. No volume tiers
that kick in at obscure thresholds. Same price for HTML, plaintext,
template, raw MIME, send-bulk — all of it.
## Currency + GST
Pricing is in **INR**. Indian partners are billed with 18% GST on
top via Zoho Billing. Non-India partners are billed in USD via the
same flow with no GST applied.
## Wallet model
The Splashify Pro Email API runs on a **prepaid wallet**:
1. Recharge your wallet through the partner panel
(`partner.splashifypro.com` → Wallet → Recharge)
2. Each successful send deducts ₹0.01 (or your override rate) from
the wallet balance
3. When balance hits zero, `/send` returns
`402 INSUFFICIENT_BALANCE` until you recharge
Recharges go through Zoho Payments — the platform's payment
processor. You get a real Zoho Billing invoice for every recharge,
emailed automatically + downloadable from the panel.
## What counts as a billable send
A send is billable when:
- The API call validates successfully
- The recipient is NOT on your suppression list
- We attempt SMTP delivery (250 OK or any 4xx/5xx response)
NOT billable:
- API rejections (`400 INVALID_REQUEST`, `400 FROM_NOT_VERIFIED`)
- Suppression-list rejections (`status: rejected` in send response)
- Sandbox sends within the daily 200 free quota
- Rate-limit rejections (`429`)
## Volume discounts
Default is one rate. For partners committing to high steady-state
volume, custom rates are negotiable — reach out via the support
form on the panel.
Admin sets per-partner overrides via `partners.email_marketing_price_override`
+ `email_transactional_price_override` columns. Once set, those
rates take precedence over the default ₹0.01.
## Tracking spend
Every send writes a row to your wallet billing log — visible at:
```
partner.splashifypro.com → Wallet → Transaction history
```
Each row shows the recipient, message_id, rate at send-time, and
balance before/after. Same data is exposed via API:
```bash
curl 'https://api.splashifypro.com/api/v1/wallet/transactions/$PARTNER_ID' \
-H "Authorization: Bearer $SPLASHIFY_API_KEY"
```
## Recharge minimums
Minimum recharge is ₹500. Wallet balance can drop below ₹500
between recharges — we don't gate sends on balance until it hits
exactly zero.
## Dedicated IP
For partners sending high steady-state volume (typically 10K+/day),
a dedicated sending IP isolates your reputation from the shared
pool. Request one from the partner panel under **Dedicated IP**.
| Item | Rate |
|---|---|
| **Dedicated IP** | **₹350 / month per IP** |
Charged via Zoho Billing on a monthly recurring invoice from the
date the IP is assigned. Per-email send pricing (₹0.01) is unchanged
— dedicated IP is purely an infrastructure add-on. Cancel anytime;
billing stops at the end of the current billing month and the IP
is returned to the shared pool.
## Postpaid (legacy)
If you're an older partner on a postpaid wallet (`wallet=Post Paid`),
sends accrue against your `outstanding` balance up to your
`credit_limit`. Reconciled monthly via a Zoho invoice. Newer
accounts ship as prepaid by default — postpaid is grandfathered for
existing partners.
## What we don't charge for
- Verifying identities
- Creating configuration sets / templates / event destinations
- Webhook deliveries to your endpoint
- Suppression list management
- Reading stats / quotas / events
- API rate limit when you stay under the per-second peak
The wallet only deducts on **actual sends**.
## Invoices + GST documents
Every recharge generates:
- A Zoho Billing invoice (PDF, downloadable from the partner panel)
- An automatic email to your account email
- Payment record marked PAID in Zoho once the wallet credit lands
For India-based partners with a GSTIN, the invoice carries your
GSTIN + state code — usable for input-tax-credit reconciliation.
## Refunds
- **Wallet balance** is non-refundable as cash. Once recharged, the
balance is yours to spend on email sends.
- **Failed sends** (4xx/5xx) are not billed in the first place.
- **Mistaken duplicate recharges** — contact support, we'll
reconcile via the existing duplicate-payment guard tables.
- **Account closure** — unused balance can be donated to a future
account with the same email or refunded under exceptional
circumstances. Contact support.
## Comparison
| Provider | Per-email rate | Free tier |
|---|---|---|
| **Splashify Pro** | ₹0.01 | 200/day in sandbox |
| AWS SES (sending alone) | ~$0.0001 (~₹0.008) | 62K/month from EC2 |
| Resend | ~$0.0001 (~₹0.008) | 100/day |
| Postmark | ~$0.0015 (~₹0.12) | 100/month |
| SendGrid | ~$0.0007–$0.001 | 100/day |
We come in cheaper than Postmark / SendGrid + competitive with the
direct AWS SES rate, with the partner-resold benefits (panel,
support, simpler onboarding) layered on top.
========================================================================
## Webhooks
URL: https://partner-docs.splashifypro.com/webhooks
========================================================================
# Webhooks
Webhooks let your app react to email lifecycle events as they
happen. Send → Delivery → Open → Click → Bounce → Complaint → Reply
— every state transition fires an HTTP POST to your URL with a
structured JSON payload.
## Two webhook surfaces
Pick whichever fits your integration:
| Surface | Where you set it | Scope | Best for |
|---|---|---|---|
| **Account-level webhook** | Settings → Webhook URL on the partner panel | Every event for every send + lifecycle events (production-access approval, account deletion, etc) | Single endpoint that handles everything; simplest setup |
| **Per-config-set destinations** | `POST /partner/email/configuration-sets/:id/event-destinations` | Only events scoped to that config set, filtered by `matching_event_types` | Per-customer or per-product routing; advanced filtering |
**You can use both at once.** The account-level URL receives every
event regardless of config set; the per-config-set destinations
receive their filtered subset additionally. Most partners start with
just the account-level URL and add per-config-set destinations later
when they need to route per-customer.
## Event types fired to your webhook
| Event | When | Surface |
|---|---|---|
| `email.sent` | Recipient MX accepted the message (250 OK) | both |
| `email.delivered` | Same as sent — direct-MX collapses these | both |
| `email.bounced` | Hard or soft bounce reported via DSN | both |
| `email.complained` | Recipient marked as spam (FBL) | both |
| `email.opened` | Recipient opened the email (1×1 pixel loaded) | both |
| `email.clicked` | Recipient clicked a link in the email | both |
| `email.rejected` | Send refused at the API layer (suppression / quota) | both |
| `email.replied` | **Recipient replied to the email** | both |
| `production_access.approved` | Admin lifted your sandbox cap | account-level only |
| `production_access.denied` | Admin denied your sandbox lift request | account-level only |
| `account.deletion.requested` | You requested account deletion | account-level only |
| `account.deletion.cancelled` | You cancelled the deletion within the 48h window | account-level only |
| `rcs_kyc.approved` | Admin approved a customer's [RCS KYC](/api-reference/customers/apply-rcs) | account-level only |
| `rcs_kyc.rejected` | Admin rejected a customer's RCS KYC (see `data.rejection_reason`) | account-level only |
| `rcs.message.text` | End-user replied with text — see [RCS Incoming](/webhooks/rcs-incoming) | account-level only |
| `rcs.message.media` | End-user sent media (image / video / PDF) | account-level only |
| `rcs.message.location` | End-user shared a location | account-level only |
| `rcs.suggestion.response` | End-user tapped a reply / URL / dial / calendar suggestion | account-level only |
| `rcs.status.sent` | Message you sent via [send-rcs](/api-reference/customers/send-rcs) was accepted by the carrier | account-level only |
| `rcs.status.delivered` | Delivered to the recipient's device | account-level only |
| `rcs.status.read` | Recipient opened the message | account-level only |
| `rcs.status.failed` | Carrier-side failure — see `data.rm_event.failure_reason` | account-level only |
| `WABA_ONBOARDED` | Customer completed WhatsApp embedded signup — see [WhatsApp Events](/webhooks/whatsapp-events) | account-level only |
| WhatsApp `sent` / `delivered` / `read` / `failed` / `deleted` | Outbound message status — raw Meta envelope, forwarded as-is | account-level only |
| WhatsApp inbound (`text` / `image` / `video` / `audio` / `document` / `sticker` / `location` / `contacts` / `interactive` / `button` / `order` / `reaction` / `system` / `unknown`) | Customer-initiated messages — raw Meta envelope, forwarded as-is | account-level only |
## How it works
```mermaid
sequenceDiagram
participant API as Splashify API
participant Worker as Webhook dispatcher
participant Hook as Your endpoint
API->>Worker: Event recorded
Worker->>Worker: Find matching destination
(by config_set + event type)
Worker->>Hook: POST signed JSON
alt 2xx
Worker->>Worker: mark delivered
else 5xx / timeout
Worker->>Worker: schedule retry
(1m, 5m, 15m)
else 4xx
Worker->>Worker: gave up
(your URL is broken)
end
```
1. You **create a configuration set** (a logical grouping for
sends).
2. You **add a webhook event destination** to that config set, with
the URL + secret + the event types you want.
3. You **send emails** with `configuration_set_name` referencing
that set.
4. Every event for those sends gets POSTed to your URL with an
HMAC-signed body.
5. **Retries:** 5xx + timeout → 1min, 5min, 15min. 4xx → no retry
(your URL is broken; we don't waste retries).
## Set up a webhook in 3 calls
### 1. Create a config set
```bash
curl https://api.splashifypro.com/api/v1/partner/email/configuration-sets \
-H "Authorization: Bearer $SPLASHIFY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "production",
"suppression_options": "BOUNCE_AND_COMPLAINT"
}'
```
Save the `config_set_id` from the response.
### 2. Add a webhook destination
```bash
curl https://api.splashifypro.com/api/v1/partner/email/configuration-sets/$CONFIG_SET_ID/event-destinations \
-H "Authorization: Bearer $SPLASHIFY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "production-webhook",
"destination_type": "WEBHOOK",
"webhook_url": "https://yourapp.com/webhooks/splashify",
"webhook_secret": "your-shared-secret-32-chars",
"matching_event_types": ["send", "delivered", "bounce", "complaint", "open", "click"]
}'
```
> **Security:** Store the `webhook_secret` somewhere your endpoint
> can read it (env var). The API will never echo it back — to
> rotate, PATCH a new value.
### 3. Reference the config set on send
```bash
curl https://api.splashifypro.com/api/v1/partner/email/send \
-H "Authorization: Bearer $SPLASHIFY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "hello@yourcompany.com",
"to": ["customer@example.com"],
"subject": "Welcome",
"html_body": "Hi
",
"configuration_set_name": "production"
}'
```
Within seconds your endpoint receives a `Send` event, then
`Delivery`, etc.
## Payload shape
We follow the **AWS SES event-publishing JSON envelope**. Code
written against AWS SES SNS subscriptions consumes our webhooks by
swapping the auth header verification.
```json
{
"eventType": "Delivery",
"mail": {
"timestamp": "2026-05-03T12:34:56Z",
"messageId": "550e8400-e29b-41d4-a716-446655440000",
"source": "hello@yourcompany.com",
"destination": ["customer@example.com"]
},
"delivery": {
"timestamp": "2026-05-03T12:34:57Z",
"recipients": ["customer@example.com"],
"smtpResponse": "250 OK"
}
}
```
Every event has the top-level `eventType` and `mail` fields. The
event-specific payload is nested under a key matching the lowercase
event type — `delivery`, `bounce`, `complaint`, etc.
## Headers
Every POST carries:
| Header | Purpose |
|---|---|
| `Content-Type` | `application/json` |
| `User-Agent` | `Splashify-Pro-Webhook/1.0` |
| `X-Splashify-Event` | Event type — `Send`, `Delivery`, `Bounce`, ... |
| `X-Splashify-Signature` | `sha256=` HMAC-SHA256 of the raw body keyed by your `webhook_secret` |
| `X-Splashify-Timestamp` | Unix seconds — protects against replay |
| `X-Splashify-Delivery-ID` | UUID per delivery attempt — use for idempotency |
## Quick verify in Node
```js
app.post("/webhooks/splashify", express.raw({ type: "application/json" }), (req, res) => {
const signature = req.header("x-splashify-signature") || "";
const expected = "sha256=" + crypto
.createHmac("sha256", process.env.SPLASHIFY_WEBHOOK_SECRET)
.update(req.body)
.digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).end();
}
const event = JSON.parse(req.body.toString());
// process event...
res.status(200).end();
});
```
## Next
- [**Event Types →**](/webhooks/event-types) — full payload shape per event
- [**Verify Signature →**](/webhooks/verify-signature) — implementations in 6 languages
- [**Retries & Replays →**](/webhooks/retries) — backoff schedule + manual replay
- [**Best Practices →**](/webhooks/best-practices) — idempotency, dedup, security
========================================================================
## Best Practices
URL: https://partner-docs.splashifypro.com/webhooks/best-practices
========================================================================
# Webhook Best Practices
## 1. Verify the signature on every request
Constant-time HMAC compare. See [Verify Signature](/webhooks/verify-signature)
for per-language code.
## 2. Ack fast, process async
Your endpoint has 10 seconds before we time out + retry. The retry
DOES NOT mean the first call failed — we have no idea until we
get a response. Best pattern:
```js
app.post("/webhooks/splashify", express.raw({...}), async (req, res) => {
// 1. Verify signature
// 2. Push to internal queue
await queue.push({ rawBody: req.body.toString(), receivedAt: Date.now() });
// 3. Ack immediately
res.status(200).end();
});
```
Process the queue with retries inside your own infrastructure.
Your endpoint becomes a fast Layer-7 doorman.
## 3. Dedup by mail.messageId + eventType
Retried deliveries have the same logical content but different
`X-Splashify-Delivery-ID` headers. Use the body fields as your dedup
key:
```js
const key = `${body.mail.messageId}:${body.eventType}`;
```
## 4. Handle out-of-order delivery
The dispatcher fans out one POST per (event, destination) combo
in parallel — events for the same email may arrive in any order.
A `Bounce` event might land before the `Send` event for the same
`messageId` if the bounce was inline.
Don't assume `Send → Delivery → Open → Click` arrive in order.
Drive your state machine off the absolute event types, not their
sequence.
## 5. Monitor your endpoint's health
Webhook delivery metrics are available via:
```bash
curl 'https://api.splashifypro.com/api/v1/partner/email/events?day_bucket=$(date -u +%Y-%m-%d)' \
-H "Authorization: Bearer $SPLASHIFY_API_KEY"
```
Track:
- **Delivery rate** — % of events delivered to your endpoint
- **Average latency** — `delivered_at - created_at`
- **Failed events** — events with `status=failed` or `gave_up`
A sudden drop in delivery rate usually means your endpoint started
returning 5xx — check your app logs.
## 6. Don't expose your webhook URL publicly
The HMAC signature stops forged events, but exposing the URL still
attracts unwanted traffic (probes, fuzzing). Guard at the network
edge:
- Only allow `POST` (not GET / OPTIONS)
- Whitelist our outbound IP range if your firewall supports it.
IPs are published at [status.splashifypro.com/ip-ranges](https://status.splashifypro.com)
and updated when new sending capacity is added — subscribe to
the changelog so you don't miss additions.
- Reject any request without `X-Splashify-Signature`
## 7. Use one webhook per concern
Don't have one giant webhook handler that branches on `eventType`.
Have separate config-set destinations for distinct concerns:
```
config_set "production-engagement" → /webhooks/engagement
→ matching_event_types: ["open", "click"]
config_set "production-deliverability" → /webhooks/deliverability
→ matching_event_types: ["bounce", "complaint", "reject"]
config_set "production-archive" → /webhooks/archive
→ matching_event_types: ["send", "delivered", "bounce", "complaint", "open", "click", "reject"]
```
Smaller handlers = easier to reason about + scope failures.
## 8. Log raw payloads for the first 30 days
Keep raw event bodies + headers in your logs (with the `webhook_secret`
redacted). When something looks weird, you have the original payload
to diff against your handler's behaviour.
## 9. Replay before going to production
Use a tool like [Webhook.site](https://webhook.site) to point
test sends at — verify your signature check, JSON parsing, and
event-type dispatch logic before pointing your real endpoint at us.
## 10. Have a fallback
Webhooks can drop. If your business depends on knowing whether an
email delivered, periodically poll `/emails/:message_id` for any
message you sent in the last hour that hasn't reached a terminal
state via webhook. Reconcile the difference.
========================================================================
## Event Types
URL: https://partner-docs.splashifypro.com/webhooks/event-types
========================================================================
# Event Types
The Email API fires nine event types. Subscribe to all of them or
just the ones you care about via the `matching_event_types` array
on the destination config.
| Event | When it fires | Header value |
|---|---|---|
| [`Send`](#send) | We accept the API call + queue the email | `Send` |
| [`Delivery`](#delivery) | Recipient MX returns 250 OK | `Delivery` |
| [`Bounce`](#bounce) | Hard or soft bounce | `Bounce` |
| [`Complaint`](#complaint) | Recipient marks as spam (FBL report) | `Complaint` |
| [`Open`](#open) | Recipient opens the email (pixel loaded) | `Open` |
| [`Click`](#click) | Recipient clicks a link in the email | `Click` |
| [`Reject`](#reject) | We refused to send (suppression list, etc.) | `Reject` |
| [`RenderingFailure`](#renderingfailure) | Template variable substitution failed | `RenderingFailure` |
| [`DeliveryDelay`](#deliverydelay) | Soft bounce — will retry | `DeliveryDelay` |
Every payload starts with the same envelope:
```json
{
"eventType": "",
"mail": {
"timestamp": "ISO8601",
"messageId": "uuid",
"source": "from-address",
"destination": ["to-address"]
},
"": { ... }
}
```
Below: the event-specific block for each type.
## Send
Fires when the API accepts your call. The email is now in the queue
— no MX attempt yet.
```json
{
"eventType": "Send",
"mail": {
"timestamp": "2026-05-03T12:34:56.123Z",
"messageId": "550e8400-e29b-41d4-a716-446655440000",
"source": "hello@yourcompany.com",
"destination": ["customer@example.com"]
},
"send": {}
}
```
## Delivery
Fires when the recipient's mailbox provider accepts the email
(250 OK). This is the strongest delivery signal — the receiving
server has accepted the message for the recipient's mailbox.
```json
{
"eventType": "Delivery",
"mail": { ... },
"delivery": {
"timestamp": "2026-05-03T12:34:57.456Z",
"recipients": ["customer@example.com"],
"smtpResponse": "250 OK"
}
}
```
## Bounce
Hard bounce (`Permanent`) means the address is dead — recipient is
auto-added to your suppression list. Soft bounce (`Transient`) means
mailbox-full / server-down / etc. — we retry up to 3 times before
giving up.
```json
{
"eventType": "Bounce",
"mail": { ... },
"bounce": {
"timestamp": "2026-05-03T12:34:58.789Z",
"bounceType": "Permanent",
"bounceSubType": "no_such_user",
"bouncedRecipients": [
{
"emailAddress": "deadbox@example.com",
"diagnosticCode": "550 5.1.1 user unknown"
}
]
}
}
```
## Complaint
Recipient marked the email as spam. We received the FBL (feedback-
loop) report from their inbox provider and auto-suppressed the
address. **Complaints are critical to monitor** — high complaint
rates trigger reputation-based sending pauses.
```json
{
"eventType": "Complaint",
"mail": { ... },
"complaint": {
"timestamp": "2026-05-03T12:35:10.123Z",
"complaintFeedbackType": "abuse",
"complainedRecipients": [
{ "emailAddress": "customer@example.com" }
]
}
}
```
## Open
Recipient opened the email — the 1×1 tracking pixel loaded. Privacy-
focused mail clients (Apple Mail, etc.) pre-load the pixel
proactively, so opens are a directional signal, not an exact one.
```json
{
"eventType": "Open",
"mail": { ... },
"open": {
"timestamp": "2026-05-03T12:36:00.000Z",
"ipAddress": "203.0.113.42",
"userAgent": "Mozilla/5.0 (iPhone; ...)"
}
}
```
## Click
Recipient clicked a tracked link. We rewrite every `` in the
HTML body through a redirect proxy that records the click + 302s to
the original URL. Unsubscribe links are NOT tracked.
```json
{
"eventType": "Click",
"mail": { ... },
"click": {
"timestamp": "2026-05-03T12:37:00.000Z",
"ipAddress": "203.0.113.42",
"userAgent": "Mozilla/5.0 (Macintosh; ...)",
"link": "https://yourapp.com/dashboard"
}
}
```
## Reject
We refused to send. Common reasons: recipient on suppression list,
sandbox limit reached, sending paused.
```json
{
"eventType": "Reject",
"mail": { ... },
"reject": {
"reason": "address on suppression list"
}
}
```
## Reply
A recipient replied to one of your emails. We catch replies via a
unique `Reply-To: reply+@mail.splashifypro.com` we stamp on
every outbound (token encodes the original `outbox_id`). When the
reply lands at our inbound listener, we parse it, look up which
outbound it's responding to, and fire this event.
```json
{
"eventType": "Reply",
"mail": { ... },
"reply": {
"outbox_id": "f8c5def1-1234-5678-9abc-def012345678",
"original_recipient": "customer@example.com",
"from": "Customer Name ",
"subject": "Re: Your order has shipped",
"message_id": "",
"in_reply_to": "",
"references": "",
"text_body": "Thanks! When can I expect delivery?...",
"html_body": "Thanks! When can I expect...",
"received_at": "2026-05-04T09:15:30.123Z"
}
}
```
`text_body` is truncated to 32 KB and `html_body` to 64 KB — long
quoted-thread replies stay inside the webhook envelope without
bloating it. Use `in_reply_to` + `references` for thread correlation
on your side.
**Disabling reply capture.** If you'd rather replies go directly to
the address in your `From` header (or your own `Reply-To` if you
set one), pass `Reply-To: your-inbox@yourcompany.com` on your send
request — when the field is non-empty we won't override it. Replies
then route directly to your address and we never see them.
> Replies that were already in flight when you change the setting
> still arrive at the original `reply+` address until the
> recipient updates their thread.
## RenderingFailure
Template variable substitution failed — `{{variable}}` referenced
in template body wasn't supplied at send time, OR was malformed.
```json
{
"eventType": "RenderingFailure",
"mail": { ... },
"renderingFailure": {
"templateName": "welcome",
"errorMessage": "missing required variable: first_name"
}
}
```
## DeliveryDelay
Soft bounce — we'll retry. Fires once on the first retry-eligible
soft bounce so you can surface a "delivery delayed" UI without
waiting for the final outcome.
```json
{
"eventType": "DeliveryDelay",
"mail": { ... },
"deliveryDelay": {
"timestamp": "2026-05-03T12:34:59.000Z",
"delayType": "TemporaryFailure",
"delayedRecipients": ["customer@example.com"]
}
}
```
## Subscribing to a subset
Want only bounces and complaints? Set `matching_event_types` on the
destination:
```bash
curl ... -d '{
"name": "deliverability-alerts",
"destination_type": "WEBHOOK",
"webhook_url": "https://yourapp.com/webhooks/deliverability",
"webhook_secret": "...",
"matching_event_types": ["bounce", "complaint"]
}'
```
Empty array = subscribe to everything. Specific list = events not
on the list are skipped.
## Multiple destinations per config set
You can attach multiple webhook destinations to a single config set
— one for engagement events (open / click), one for deliverability
alerts (bounce / complaint), one for archival (everything). Each
destination delivers + retries independently.
========================================================================
## RCS Events
URL: https://partner-docs.splashifypro.com/webhooks/rcs-events
========================================================================
# RCS Events
When a partner customer applies for RCS via
[`POST /partner/customers/{customer_id}/rcs/apply`](/api-reference/customers/apply-rcs),
the submission queues for admin review. The moment admin approves or
rejects it, we POST an event to the **account-level webhook URL** you
configured under Settings → Webhook URL on the partner panel.
These events do **not** flow through the per-config-set destinations —
that surface is email-only. RCS events use the same channel as
`production_access.approved` and `account.deletion.requested`.
## Event types
| Event | When |
|---|---|
| `rcs_kyc.approved` | Admin approved the customer's RCS KYC. RCS sender registration follows. |
| `rcs_kyc.rejected` | Admin rejected the submission. `data.rejection_reason` explains why. Re-call `apply` with a corrected payload to resubmit. |
## Envelope
Account-level webhooks share this envelope (different from the email
events' SES-shaped envelope — same headers, different body):
```json
{
"event_type": "rcs_kyc.approved",
"partner_id": "11111111-2222-3333-4444-555555555555",
"occurred_at": "2026-05-21T07:04:11.482Z",
"data": { /* event-specific, see below */ }
}
```
## `rcs_kyc.approved` payload
```json
{
"event_type": "rcs_kyc.approved",
"partner_id": "11111111-2222-3333-4444-555555555555",
"occurred_at": "2026-05-21T07:04:11.482Z",
"data": {
"customer_id": "8f3b2a1e-49d8-4c2d-9e1a-b7e6d5f8a3c2",
"customer_name": "Acme Corporation",
"customer_email": "contact@acme.com",
"customer_phone": "+919876543210",
"status": "approved",
"rejection_reason": "",
"reviewed_by": "ops@evolvepro.tech",
"reviewed_at": "2026-05-21T07:04:11.482Z",
"submitted_at": "2026-05-20T10:18:42.000Z"
}
}
```
## `rcs_kyc.rejected` payload
```json
{
"event_type": "rcs_kyc.rejected",
"partner_id": "11111111-2222-3333-4444-555555555555",
"occurred_at": "2026-05-21T07:04:11.482Z",
"data": {
"customer_id": "8f3b2a1e-49d8-4c2d-9e1a-b7e6d5f8a3c2",
"customer_name": "Acme Corporation",
"customer_email": "contact@acme.com",
"customer_phone": "+919876543210",
"status": "rejected",
"rejection_reason": "Bot logo is 512×512; we need 224×224 JPEG ≤ 50 KB.",
"reviewed_by": "ops@evolvepro.tech",
"reviewed_at": "2026-05-21T07:04:11.482Z",
"submitted_at": "2026-05-20T10:18:42.000Z"
}
}
```
## Headers
Same headers as all account-level webhooks:
| Header | Value / purpose |
|---|---|
| `Content-Type` | `application/json` |
| `User-Agent` | `Splashify-Pro-Webhook/1.0` |
| `X-Splashify-Event` | `rcs_kyc.approved` or `rcs_kyc.rejected` |
| `X-Splashify-Delivery-ID` | UUID per delivery attempt — use for idempotency |
| `X-Splashify-Timestamp` | Unix seconds — replay protection |
> Signature verification (`X-Splashify-Signature`) is only emitted by
> the email-events surface today. RCS events arrive unsigned over
> HTTPS; whitelist the source by domain if you need stricter
> authentication.
## Handler example (Node)
```js
app.post("/webhooks/splashify", express.json(), (req, res) => {
const event = req.headers["x-splashify-event"];
const { partner_id, occurred_at, data } = req.body;
switch (event) {
case "rcs_kyc.approved":
await db.customers.update(data.customer_id, {
rcs_status: "approved",
rcs_approved_at: data.reviewed_at,
});
break;
case "rcs_kyc.rejected":
await db.customers.update(data.customer_id, {
rcs_status: "rejected",
rcs_rejection_reason: data.rejection_reason,
});
await notifyTeam(`RCS rejected for ${data.customer_email}: ${data.rejection_reason}`);
break;
}
res.status(200).end();
});
```
## Delivery semantics
- **Fire-and-forget.** Admin's approve/reject action commits regardless
of whether your webhook returns 2xx. A failed delivery doesn't
block the status flip.
- **No retries on this surface.** Account-level webhooks log 4xx/5xx
responses but do not retry. Keep your endpoint healthy, or poll
[`GET /partner/customers/{customer_id}/rcs/status`](/api-reference/customers/rcs-status)
as a fallback.
- **10 s timeout.** If your endpoint takes longer to respond, the
request is cancelled and logged as a delivery failure.
- **Order is not guaranteed.** Don't assume `rcs_kyc.approved` arrives
before `rcs_kyc.rejected` for the same customer in a re-review
scenario. Always trust `data.status` and `occurred_at`.
## Idempotency
Same delivery ID retried (e.g. by a manual replay tool — coming) will
carry the same `X-Splashify-Delivery-ID`. De-dupe on it.
For the underlying event itself, the tuple
`(partner_id, data.customer_id, data.status, occurred_at)` is unique —
two reviews of the same submission produce two events with different
`occurred_at`.
========================================================================
## RCS Incoming Events
URL: https://partner-docs.splashifypro.com/webhooks/rcs-incoming
========================================================================
# RCS Incoming Events
Every Route Mobile callback for a customer that is **RCS-Active**
under your partner account is forwarded to your account-level
**Webhook URL** (set under Settings → Webhook URL on the partner
panel).
There are two sources of these callbacks:
- **Inbound messages** — the end-user sent a text, media file,
location, or tapped a suggestion.
- **Status updates** — a message you sent via
[`POST …/customers/{customer_id}/rcs/send`](/api-reference/customers/send-rcs)
changed state (sent → delivered → read, or failed).
Inbound events are routed to your partner account based on the
customer the message was sent to. You only ever see events for
customers under your account — there is no cross-tenant leakage.
## Event types
| Event | When | RM event_type |
|---|---|---|
| `rcs.message.text` | End-user replied with text | `text_message` |
| `rcs.message.media` | End-user sent image / video / PDF / other file | `media_message` |
| `rcs.message.location` | End-user shared a location | `location_message` |
| `rcs.suggestion.response` | End-user tapped a suggestion (reply / URL / dial / calendar / location) | `response` |
| `rcs.status.sent` | Message you sent was accepted by the recipient's carrier | `status` + `SENT` |
| `rcs.status.delivered` | Message was delivered to the recipient's device | `status` + `DELIVERED` |
| `rcs.status.read` | Recipient opened the message | `status` + `READ` |
| `rcs.status.failed` | Message could not be delivered | `status` + `FAILED` |
| `rcs.status.unknown` | RM emitted a message_status we don't recognise — surface verbatim | `status` + other |
## Envelope
Same envelope as the rest of the account-level webhooks (production
access, account deletion, RCS KYC, etc.):
```json
{
"event_type": "rcs.message.text",
"partner_id": "11111111-2222-3333-4444-555555555555",
"occurred_at": "2026-05-20T08:26:32.082Z",
"data": {
"customer_id": "8f3b2a1e-49d8-4c2d-9e1a-b7e6d5f8a3c2",
"bot_name": "SplashifyPro_promo",
"rm_event": { /* unmodified Route Mobile payload — see below */ }
}
}
```
`data.rm_event` is the **raw** body Route Mobile delivered to us. The
normalised fields we extract for routing live alongside (`customer_id`,
`bot_name`), but everything else is in `rm_event` for partners who want
to read RM's payload directly without us getting in the way.
## Inbound payloads
### `rcs.message.text`
```json
{
"event_type": "rcs.message.text",
"partner_id": "...",
"occurred_at": "2026-05-20T08:26:32.082Z",
"data": {
"customer_id": "...",
"bot_name": "SplashifyPro_promo",
"rm_event": {
"bot_name": "SplashifyPro_promo",
"event_type": "text_message",
"request_id": "MxbipjUYLMRD27ZaJRMaSKgA2",
"session_id": "ecfd6ac3-3c9c-4dfc-97df-9e759c2a93d5",
"text_message": "Hi",
"timestamp": "2026-05-20T08:26:32.080697Z",
"user_contact": "+919876543210",
"username": "demo"
}
}
}
```
### `rcs.message.media`
`rm_event.media_type` is one of `image`, `video`, `application`
(documents). `media_uri` is a temporary Google RCS blob URL — fetch
within minutes if you want to persist the file.
```json
{
"event_type": "rcs.message.media",
"partner_id": "...",
"occurred_at": "2026-05-20T08:27:44.448Z",
"data": {
"customer_id": "...",
"bot_name": "SplashifyPro_promo",
"rm_event": {
"bot_name": "SplashifyPro_promo",
"event_type": "media_message",
"media_name": "5023611655770832826.jpg",
"media_size": "67541",
"media_type": "image",
"media_uri": "https://rcs-copper-ap.googleapis.com/blob/...",
"request_id": "MxWpHXTmckS3GRVTVa6VMydg2",
"session_id": "ecfd6ac3-3c9c-4dfc-97df-9e759c2a93d5",
"timestamp": "2026-05-20T08:27:44.445575Z",
"user_contact": "+919876543210",
"username": "demo"
}
}
}
```
### `rcs.message.location`
```json
{
"event_type": "rcs.message.location",
"partner_id": "...",
"occurred_at": "2026-05-20T08:28:25.144Z",
"data": {
"customer_id": "...",
"bot_name": "SplashifyPro_promo",
"rm_event": {
"bot_name": "SplashifyPro_promo",
"event_type": "location_message",
"latitude": "12.972652",
"longitude": "77.7110861",
"request_id": "Mx93B8zFOdSzCySLkCcHxxIQ2",
"session_id": "ecfd6ac3-3c9c-4dfc-97df-9e759c2a93d5",
"timestamp": "2026-05-20T08:28:25.142098Z",
"user_contact": "+919876543210",
"username": "demo"
}
}
}
```
### `rcs.suggestion.response`
End-user tapped one of the suggestions you attached to a previous
message. `rm_event.suggestion_type` is `reply`, `action` (URL or dial),
or `view_location`. The `response_postback` matches the `postback` you
sent with the suggestion — use it as a stable correlation key.
```json
{
"event_type": "rcs.suggestion.response",
"partner_id": "...",
"occurred_at": "2026-05-20T10:49:08.724Z",
"data": {
"customer_id": "...",
"bot_name": "SplashifyPro_promo",
"rm_event": {
"bot_name": "SplashifyPro_promo",
"event_type": "response",
"request_id": "MxU4UwnudnTuuexqE6ZSYeDQ2",
"response_postback": "track",
"response_text": "Track order",
"session_id": "ecfd6ac3-3c9c-4dfc-97df-9e759c2a93d5",
"suggestion_type": "action",
"timestamp": "2026-05-20T10:49:08.720679Z",
"user_contact": "+919876543210",
"username": "demo"
}
}
}
```
## Status payloads
Status events reference the `message_id` you got back from the send
call. Use it as your join key against your own outbound table.
### `rcs.status.sent` / `delivered` / `read`
```json
{
"event_type": "rcs.status.delivered",
"partner_id": "...",
"occurred_at": "2026-05-25T12:58:13.683Z",
"data": {
"customer_id": "...",
"bot_name": "SplashifyPro_promo",
"rm_event": {
"bot_name": "SplashifyPro_promo",
"event_type": "status",
"message_id": "61c11dfe-5839-11f1-a96f-0a58a9feac021",
"message_status": "DELIVERED",
"timestamp": "2026-05-25T12:58:13.683308Z",
"user_contact": "+917014311953",
"username": "EvolveRCS",
"extra": "lifecycle_trial_expired"
}
}
}
```
`sent` and `read` payloads are identical apart from the `event_type`
and the `rm_event.message_status` field.
#### About `rm_event.extra` — your passthrough field
Whatever string you passed in the `extra` field of the original
[Send RCS](/api-reference/customers/send-rcs) call comes back here
verbatim on every status callback. We don't interpret, modify, or
truncate it. Common uses:
- **Idempotency key** — set `extra` to a UUID per logical send; if
network retries cause the same logical message to send twice, you
see the same `extra` on both delivery callbacks and dedupe on your
side.
- **Lifecycle tag** — set `extra` to your internal campaign /
segment name (e.g. `lifecycle_trial_expired`, `cart_abandon_24h`).
Pivot delivery rates by tag without a separate join table.
- **Trace ID** — set `extra` to the trace ID from your job system,
so the callback lands in the same trace as the originating send.
If you don't send `extra`, the field is omitted from the callback.
### `rcs.status.failed`
```json
{
"event_type": "rcs.status.failed",
"partner_id": "...",
"occurred_at": "2026-05-20T08:12:12.903Z",
"data": {
"customer_id": "...",
"bot_name": "SplashifyPro_promo",
"rm_event": {
"bot_name": "SplashifyPro_promo",
"event_type": "status",
"failure_reason": "user is not in a conversation and provided message template is not approved",
"message_id": "faea56dc-2b1a-11f0-a4b9-0a58a9feac02",
"message_status": "FAILED",
"timestamp": "2026-05-20T08:12:12.900Z",
"user_contact": "+919876543210",
"username": "demo",
"extra": "cart_abandon_24h"
}
}
}
```
`rm_event.extra` is also echoed on failure callbacks, so your
idempotency / lifecycle / trace tagging works end-to-end even when
the send doesn't reach the recipient.
`failure_reason` is the carrier-side reason. Common values:
- *"user is not in a conversation and provided message template is not approved"* — recipient hasn't opted in / your bot is sandboxed.
- *"user device does not support RCS"* — fall back to SMS / WhatsApp.
- *"invalid recipient number"* — your `phone_no` was rejected.
## Headers
Same headers as every account-level webhook:
| Header | Value / purpose |
|---|---|
| `Content-Type` | `application/json` |
| `User-Agent` | `Splashify-Pro-Webhook/1.0` |
| `X-Splashify-Event` | `rcs.message.text` / `rcs.status.delivered` / ... |
| `X-Splashify-Delivery-ID` | UUID per delivery attempt — idempotency key |
| `X-Splashify-Timestamp` | Unix seconds — replay protection |
> Signature verification is not emitted on this surface today. Pin the
> source by hostname (`api.splashifypro.com`) if you need stricter
> auth.
## Node handler example
```js
app.post("/webhooks/splashify", express.json(), async (req, res) => {
const event = req.headers["x-splashify-event"];
const { data } = req.body;
switch (event) {
case "rcs.message.text":
await store.inbound({
customer_id: data.customer_id,
from: data.rm_event.user_contact,
text: data.rm_event.text_message,
session_id: data.rm_event.session_id,
});
break;
case "rcs.suggestion.response":
// Use the postback you sent originally to route the reply
await store.handleSuggestion(data.rm_event.response_postback, data);
break;
case "rcs.status.sent":
case "rcs.status.delivered":
case "rcs.status.read":
await store.updateOutbound(data.rm_event.message_id, event.split(".").pop());
break;
case "rcs.status.failed":
await alerts.failedRCS(data.rm_event.message_id, data.rm_event.failure_reason);
break;
}
res.status(200).end();
});
```
## Delivery semantics
- **Fire-and-forget.** Route Mobile gets a 200 OK from us regardless of
whether your endpoint succeeds. A failed delivery doesn't queue a
retry — RM doesn't know about your endpoint.
- **No retries on this surface.** 4xx/5xx from your endpoint is logged
and dropped. Keep the endpoint healthy.
- **10 s timeout.** If your endpoint doesn't respond in 10 s the
request is cancelled. Acknowledge fast, process async.
- **Order is not guaranteed.** `sent`, `delivered`, and `read` for the
same `message_id` can arrive out of order. Always trust the latest
`rm_event.timestamp`.
## Idempotency
`X-Splashify-Delivery-ID` is unique per HTTP delivery attempt. For the
underlying event, use:
- inbound messages: `rm_event.request_id` (RM's own dedupe key)
- status events: `(rm_event.message_id, event_type)`
========================================================================
## Retries & Replays
URL: https://partner-docs.splashifypro.com/webhooks/retries
========================================================================
# Retries & Replays
The webhook dispatcher retries on transient failures and gives up
on permanent ones. This page covers the exact behaviour so your
endpoint can plan for it.
## Retry schedule
| Attempt | Backoff | When |
|---|---|---|
| 1 | — | Immediate (within seconds of the event) |
| 2 | 1 minute | After attempt 1 fails |
| 3 | 5 minutes | After attempt 2 fails |
| 4 | 15 minutes | After attempt 3 fails |
| Final | give up | After attempt 4 fails — no further retries |
Total retry window: ~21 minutes. After that the event is marked
`gave_up` in our audit log and no further attempts are made.
## What triggers a retry
| Response | Retry? | Reason |
|---|---|---|
| 2xx (200–299) | No | Delivered |
| 3xx | Yes | We don't follow redirects — set up your URL to respond directly |
| 4xx (except 408, 429) | **No** | Your endpoint is misconfigured / rejecting; retries waste both sides' budget |
| 408 (timeout) | Yes | Treated as transient |
| 429 (rate limit) | Yes | Backoff respects `Retry-After` header up to 1 hour |
| 5xx | Yes | Transient — recipient claimed the event but couldn't process |
| Network timeout | Yes | 10-second timeout per request |
## Why 4xx doesn't retry
Most 4xx responses indicate your endpoint is broken in a way time
won't fix:
- 401/403 — wrong shared secret
- 404 — endpoint doesn't exist
- 405 — wrong HTTP method
- 422 — payload schema mismatch
If your endpoint *needs* time to recover (e.g. you just deployed +
the route 404s for 30 seconds), respond with `503` instead of `404`
during the deploy window. We'll retry.
## Idempotency
Every webhook attempt carries a unique `X-Splashify-Delivery-ID`
header. Even retries of the same event have a fresh delivery ID.
To dedup at your end, key off `mail.messageId` + `eventType`:
```js
const key = `${body.mail.messageId}:${body.eventType}`;
const inserted = await db.events.insertOne({
_id: key,
...body,
receivedAt: new Date(),
}, { ignoreDuplicates: true });
if (!inserted.insertedCount) {
return res.status(200).end(); // already processed, ack
}
// ... process
```
The combination is unique per event — even if we retry due to your
500 response, the second delivery has the same `mail.messageId` +
`eventType` and your insert dedups it.
## Manual replay
For events that gave up (your endpoint was down for >21 minutes),
contact support to replay them. We retain the full event log for
30 days and can re-fan-out a specific window.
Self-serve replay is on the roadmap — `POST /partner/email/events/replay`
with a time range + optional event-type filter.
## Inspecting webhook delivery state
To see whether an event was delivered to a destination:
```bash
curl 'https://api.splashifypro.com/api/v1/partner/email/events?day_bucket=2026-05-03' \
-H "Authorization: Bearer $SPLASHIFY_API_KEY"
```
Response includes per-destination delivery status (delivered,
failed, gave_up) so you can see which webhooks landed.
## Common reasons for repeated failures
- **Endpoint times out > 10 seconds.** Your handler should ack
ASAP (write to a queue, return 200) and process async.
- **Endpoint behind Cloudflare with bot protection.** Bot challenges
return 403 to our requests — whitelist our user-agent
(`Splashify-Pro-Webhook/1.0`) or our outbound IP range.
- **HTTPS cert expired.** We don't disable cert verification.
Renew + monitor.
- **Wrong secret.** If you rotated the secret in the panel but
didn't roll it on your endpoint, every signature check returns
401.
========================================================================
## Open & click tracking
URL: https://partner-docs.splashifypro.com/webhooks/tracking
========================================================================
# Open & click tracking
Every email sent through Splashify Pro can carry two kinds of
recipient-side instrumentation:
| Tracker | Mechanism | Generates |
|---|---|---|
| **Open** | 1×1 transparent GIF embedded just before `