mxsin.com

API reference

The mxsin.com HTTP API sends email, reports what happened to it and manages templates, suppressions and contact lists. It is a JSON API over HTTPS. This page lists every route the API accepts.

Authentication

Base URL: https://mxsin.com/api/v1. Every request carries an API key as a bearer token:

Authorization: Bearer mx_…

Create keys in the panel under API keys & SMTP. A key is shown once, when it is created; we store only a hash of it. Keep keys on your server, never in browser or mobile code. Request bodies are JSON (Content-Type: application/json), and responses are JSON.

Limits

Errors

Errors return a JSON body such as {"error": "Need 1-50 recipients"}.

StatusMeaning
400The request is malformed or a field is invalid. The error message names the field.
401Missing or unknown API key.
403Not allowed: unverified sender domain, suspended or paused account, or a protected suppression.
404No such route or object.
413The message is larger than your plan allows.
422Content rejected, for example by the virus scanner.
429A limit was reached: requests per minute, or your daily quota. Wait and retry.
500Something failed on our side. Retry with backoff.
503A dependency is briefly unavailable. Retry shortly.

Retry 429, 500 and 503 with exponential backoff. Do not retry other 4xx errors unchanged.

Client libraries

Small single-file libraries wrap every route below, raise errors with the HTTP status, and include a signature check for webhooks and exports. They have no dependencies beyond the language’s standard library.

PHP 8 · mxsin.phpNode.js 18 · mxsin.jsPython 3.8 · mxsin.py

Webhook and export signatures

Webhooks and scheduled exports are signed. The X-Mxsin-Signature header reads t=<unix time>,v1=<hex>, where the hex value is HMAC-SHA256 of <t>.<raw body> using the endpoint’s signing secret. Compare in constant time and reject timestamps older than five minutes. Failed webhook deliveries are retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours.

POST /send

Send a message. Queues one message to up to 50 recipients (to, cc and bcc together). The From domain must be added to your account and verified. Each recipient becomes its own message, so bounces and suppressions apply per recipient; the response maps each recipient to its message id and status (queued or suppressed).

FieldTypeRequiredDescription
fromstringYes"Name <addr@yourdomain>" or a bare address on a verified domain
tostring | arrayYesOne or more recipients
cc, bccarrayNoExtra recipients
subjectstringYesRequired unless the template has one
text, htmlstringYesAt least one, unless a template provides it
reply_tostringNoReply-To address
headersobjectNoCustom X- headers
tagstringNo1–64 characters: letters, digits, _ . : -
metadataobjectNoUp to 2 KB, returned in lookups and webhooks
streamstringNoMessage stream name
templatestringNoTemplate name
variablesobjectNoValues for {{placeholders}}; HTML-escaped in the HTML part
curl -X POST 'https://mxsin.com/api/v1/send' \
  -H 'Authorization: Bearer mx_YOUR_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "from": "Your App <hello@example.com>",
    "to": [
        "customer@example.org"
    ],
    "subject": "Your receipt",
    "text": "Thanks for your order.",
    "tag": "receipt",
    "stream": "transactional"
}'
require 'mxsin.php';
$mx = new Mxsin\Client(getenv('MXSIN_API_KEY'));
$res = $mx->send([
    'from' => 'Your App <hello@example.com>',
    'to' => [
        'customer@example.org'
    ],
    'subject' => 'Your receipt',
    'text' => 'Thanks for your order.',
    'tag' => 'receipt',
    'stream' => 'transactional'
]);
const { Mxsin } = require('./mxsin.js');
const mx = new Mxsin(process.env.MXSIN_API_KEY);
const res = await mx.call('POST', '/send', {
    "from": "Your App <hello@example.com>",
    "to": [
        "customer@example.org"
    ],
    "subject": "Your receipt",
    "text": "Thanks for your order.",
    "tag": "receipt",
    "stream": "transactional"
});
import os
from mxsin import Mxsin

mx = Mxsin(os.environ['MXSIN_API_KEY'])
res = mx.call('POST', '/send', {
    "from": "Your App <hello@example.com>",
    "to": [
        "customer@example.org"
    ],
    "subject": "Your receipt",
    "text": "Thanks for your order.",
    "tag": "receipt",
    "stream": "transactional"
})

Example response

{
    "message_ids": {
        "customer@example.org": 1234
    },
    "status": {
        "customer@example.org": "queued"
    }
}

POST /send/bulk

Bulk send with a template. Sends one template to up to 500 recipients in a single call, each with their own variables, or to the subscribed contacts of a list (optionally one topic). Every recipient counts against your daily quota, and the whole call is refused if it would go over. Messages are queued and paced at your account’s sending rate.

FieldTypeRequiredDescription
fromstringYesSender on a verified domain
templatestringYesTemplate name
recipientsarrayNoUp to 500 objects: {"to": "...", "variables": {...}}
list_idintegerNoInstead of recipients: send to this list’s subscribed contacts (500 at most); contact fields become variables
topicstringNoWith list_id: only contacts opted in to this topic
default_variablesobjectNoVariables shared by every recipient (per-recipient values win)
subjectstringNoOverrides the template subject
streamstringNoMessage stream name
tagstringNoStatistics tag
curl -X POST 'https://mxsin.com/api/v1/send/bulk' \
  -H 'Authorization: Bearer mx_YOUR_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "from": "news@example.com",
    "template": "monthly-update",
    "stream": "newsletter",
    "default_variables": {
        "month": "November"
    },
    "recipients": [
        {
            "to": "alex@example.org",
            "variables": {
                "name": "Alex"
            }
        },
        {
            "to": "sam@example.org",
            "variables": {
                "name": "Sam"
            }
        }
    ]
}'
require 'mxsin.php';
$mx = new Mxsin\Client(getenv('MXSIN_API_KEY'));
$res = $mx->sendBulk([
    'from' => 'news@example.com',
    'template' => 'monthly-update',
    'stream' => 'newsletter',
    'default_variables' => [
        'month' => 'November'
    ],
    'recipients' => [
        [
            'to' => 'alex@example.org',
            'variables' => [
                'name' => 'Alex'
            ]
        ],
        [
            'to' => 'sam@example.org',
            'variables' => [
                'name' => 'Sam'
            ]
        ]
    ]
]);
const { Mxsin } = require('./mxsin.js');
const mx = new Mxsin(process.env.MXSIN_API_KEY);
const res = await mx.call('POST', '/send/bulk', {
    "from": "news@example.com",
    "template": "monthly-update",
    "stream": "newsletter",
    "default_variables": {
        "month": "November"
    },
    "recipients": [
        {
            "to": "alex@example.org",
            "variables": {
                "name": "Alex"
            }
        },
        {
            "to": "sam@example.org",
            "variables": {
                "name": "Sam"
            }
        }
    ]
});
import os
from mxsin import Mxsin

mx = Mxsin(os.environ['MXSIN_API_KEY'])
res = mx.call('POST', '/send/bulk', {
    "from": "news@example.com",
    "template": "monthly-update",
    "stream": "newsletter",
    "default_variables": {
        "month": "November"
    },
    "recipients": [
        {
            "to": "alex@example.org",
            "variables": {
                "name": "Alex"
            }
        },
        {
            "to": "sam@example.org",
            "variables": {
                "name": "Sam"
            }
        }
    ]
})

Example response

{
    "accepted": 2,
    "results": [
        {
            "to": "alex@example.org",
            "message_id": 1235,
            "status": "queued"
        },
        {
            "to": "sam@example.org",
            "message_id": 1236,
            "status": "queued"
        }
    ]
}

GET /messages/{id}

Look up a message. Status, SMTP response, opens, clicks, metadata and the full event timeline of one message.

curl -X GET 'https://mxsin.com/api/v1/messages/1234' \
  -H 'Authorization: Bearer mx_YOUR_KEY'
require 'mxsin.php';
$mx = new Mxsin\Client(getenv('MXSIN_API_KEY'));
$res = $mx->call('GET', '/messages/1234');
const { Mxsin } = require('./mxsin.js');
const mx = new Mxsin(process.env.MXSIN_API_KEY);
const res = await mx.call('GET', '/messages/1234');
import os
from mxsin import Mxsin

mx = Mxsin(os.environ['MXSIN_API_KEY'])
res = mx.call('GET', '/messages/1234')

Example response

{
    "id": 1234,
    "rcpt": "customer@example.org",
    "status": "delivered",
    "events": [
        {
            "type": "sent",
            "at": 1760000000
        },
        {
            "type": "delivered",
            "at": 1760000003
        }
    ]
}

GET /events

List events. Delivery events for your messages, oldest first. Page with after= set to the last id you received.

FieldTypeRequiredDescription
afterintegerNoReturn events with an id greater than this
typestringNosent, delivered, deferred, bounced, complained, opened, clicked or unsubscribed
limitintegerNo1–500, default 100
curl -X GET 'https://mxsin.com/api/v1/events' \
  -H 'Authorization: Bearer mx_YOUR_KEY'
require 'mxsin.php';
$mx = new Mxsin\Client(getenv('MXSIN_API_KEY'));
$res = $mx->call('GET', '/events');
const { Mxsin } = require('./mxsin.js');
const mx = new Mxsin(process.env.MXSIN_API_KEY);
const res = await mx.call('GET', '/events');
import os
from mxsin import Mxsin

mx = Mxsin(os.environ['MXSIN_API_KEY'])
res = mx.call('GET', '/events')

Example response

{
    "events": [
        {
            "id": 9001,
            "type": "delivered",
            "at": 1760000003,
            "message_id": 1234,
            "to": "customer@example.org",
            "detail": "250 2.0.0 OK"
        }
    ],
    "next_after": 9001
}

GET /templates

List templates. Names, subjects and the placeholders each template uses.

curl -X GET 'https://mxsin.com/api/v1/templates' \
  -H 'Authorization: Bearer mx_YOUR_KEY'
require 'mxsin.php';
$mx = new Mxsin\Client(getenv('MXSIN_API_KEY'));
$res = $mx->call('GET', '/templates');
const { Mxsin } = require('./mxsin.js');
const mx = new Mxsin(process.env.MXSIN_API_KEY);
const res = await mx.call('GET', '/templates');
import os
from mxsin import Mxsin

mx = Mxsin(os.environ['MXSIN_API_KEY'])
res = mx.call('GET', '/templates')

Example response

{
    "templates": [
        {
            "name": "welcome",
            "subject": "Welcome, {{name}}",
            "variables": [
                "name",
                "link"
            ],
            "updated_at": 1760000000
        }
    ]
}

GET /templates/{name}

Get a template. One template with its HTML and text parts.

curl -X GET 'https://mxsin.com/api/v1/templates/welcome' \
  -H 'Authorization: Bearer mx_YOUR_KEY'
require 'mxsin.php';
$mx = new Mxsin\Client(getenv('MXSIN_API_KEY'));
$res = $mx->call('GET', '/templates/welcome');
const { Mxsin } = require('./mxsin.js');
const mx = new Mxsin(process.env.MXSIN_API_KEY);
const res = await mx.call('GET', '/templates/welcome');
import os
from mxsin import Mxsin

mx = Mxsin(os.environ['MXSIN_API_KEY'])
res = mx.call('GET', '/templates/welcome')

Example response

{
    "name": "welcome",
    "subject": "Welcome, {{name}}",
    "html": "<p>Hi {{name}}</p>",
    "text": "Hi {{name}}",
    "variables": [
        "name"
    ]
}

GET /suppressions

List suppressions. Addresses that will not be mailed, newest first.

FieldTypeRequiredDescription
afterintegerNoReturn entries with an id lower than this (paging)
limitintegerNo1–500, default 100
curl -X GET 'https://mxsin.com/api/v1/suppressions' \
  -H 'Authorization: Bearer mx_YOUR_KEY'
require 'mxsin.php';
$mx = new Mxsin\Client(getenv('MXSIN_API_KEY'));
$res = $mx->call('GET', '/suppressions');
const { Mxsin } = require('./mxsin.js');
const mx = new Mxsin(process.env.MXSIN_API_KEY);
const res = await mx.call('GET', '/suppressions');
import os
from mxsin import Mxsin

mx = Mxsin(os.environ['MXSIN_API_KEY'])
res = mx.call('GET', '/suppressions')

Example response

{
    "suppressions": [
        {
            "id": 77,
            "email": "gone@example.org",
            "reason": "bounce",
            "created_at": 1760000000
        }
    ],
    "next_after": 77
}

POST /suppressions

Add a suppression. Stops all future mail to an address from your account.

FieldTypeRequiredDescription
emailstringYesAddress to suppress
detailstringNoNote, up to 200 characters
curl -X POST 'https://mxsin.com/api/v1/suppressions' \
  -H 'Authorization: Bearer mx_YOUR_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "email": "no-more@example.org",
    "detail": "Asked by phone"
}'
require 'mxsin.php';
$mx = new Mxsin\Client(getenv('MXSIN_API_KEY'));
$res = $mx->call('POST', '/suppressions', [
    'email' => 'no-more@example.org',
    'detail' => 'Asked by phone'
]);
const { Mxsin } = require('./mxsin.js');
const mx = new Mxsin(process.env.MXSIN_API_KEY);
const res = await mx.call('POST', '/suppressions', {
    "email": "no-more@example.org",
    "detail": "Asked by phone"
});
import os
from mxsin import Mxsin

mx = Mxsin(os.environ['MXSIN_API_KEY'])
res = mx.call('POST', '/suppressions', {
    "email": "no-more@example.org",
    "detail": "Asked by phone"
})

Example response

{
    "email": "no-more@example.org",
    "reason": "manual"
}

DELETE /suppressions/{email}

Remove a suppression. Removes a manual or bounce suppression. Complaints and unsubscribes can only be removed by the recipient (through the preference page) or by our support team.

curl -X DELETE 'https://mxsin.com/api/v1/suppressions/alex%40example.org' \
  -H 'Authorization: Bearer mx_YOUR_KEY'
require 'mxsin.php';
$mx = new Mxsin\Client(getenv('MXSIN_API_KEY'));
$res = $mx->call('DELETE', '/suppressions/alex%40example.org');
const { Mxsin } = require('./mxsin.js');
const mx = new Mxsin(process.env.MXSIN_API_KEY);
const res = await mx.call('DELETE', '/suppressions/alex%40example.org');
import os
from mxsin import Mxsin

mx = Mxsin(os.environ['MXSIN_API_KEY'])
res = mx.call('DELETE', '/suppressions/alex%40example.org')

Example response

{
    "deleted": true
}

GET /lists

List contact lists. Your lists with their topics and contact counts.

curl -X GET 'https://mxsin.com/api/v1/lists' \
  -H 'Authorization: Bearer mx_YOUR_KEY'
require 'mxsin.php';
$mx = new Mxsin\Client(getenv('MXSIN_API_KEY'));
$res = $mx->call('GET', '/lists');
const { Mxsin } = require('./mxsin.js');
const mx = new Mxsin(process.env.MXSIN_API_KEY);
const res = await mx.call('GET', '/lists');
import os
from mxsin import Mxsin

mx = Mxsin(os.environ['MXSIN_API_KEY'])
res = mx.call('GET', '/lists')

Example response

{
    "lists": [
        {
            "id": 3,
            "name": "Newsletter",
            "topics": [
                {
                    "name": "product-news",
                    "label": "Product news",
                    "default_on": true
                }
            ],
            "contacts": 1200,
            "subscribed": 1150
        }
    ]
}

POST /lists

Create a list. Creates a list, optionally with subscription topics.

FieldTypeRequiredDescription
namestringYesUp to 80 characters
topicsarrayNoTopic labels, for example ["Product news", "Events"]
curl -X POST 'https://mxsin.com/api/v1/lists' \
  -H 'Authorization: Bearer mx_YOUR_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Newsletter",
    "topics": [
        "Product news",
        "Events"
    ]
}'
require 'mxsin.php';
$mx = new Mxsin\Client(getenv('MXSIN_API_KEY'));
$res = $mx->call('POST', '/lists', [
    'name' => 'Newsletter',
    'topics' => [
        'Product news',
        'Events'
    ]
]);
const { Mxsin } = require('./mxsin.js');
const mx = new Mxsin(process.env.MXSIN_API_KEY);
const res = await mx.call('POST', '/lists', {
    "name": "Newsletter",
    "topics": [
        "Product news",
        "Events"
    ]
});
import os
from mxsin import Mxsin

mx = Mxsin(os.environ['MXSIN_API_KEY'])
res = mx.call('POST', '/lists', {
    "name": "Newsletter",
    "topics": [
        "Product news",
        "Events"
    ]
})

Example response

{
    "id": 3,
    "name": "Newsletter"
}

GET /lists/{id}/contacts

List contacts. Contacts on a list with their status and topic choices. Status is subscribed, unsubscribed or suppressed.

FieldTypeRequiredDescription
pageintegerNoPage number, 100 per page
curl -X GET 'https://mxsin.com/api/v1/lists/3/contacts' \
  -H 'Authorization: Bearer mx_YOUR_KEY'
require 'mxsin.php';
$mx = new Mxsin\Client(getenv('MXSIN_API_KEY'));
$res = $mx->call('GET', '/lists/3/contacts');
const { Mxsin } = require('./mxsin.js');
const mx = new Mxsin(process.env.MXSIN_API_KEY);
const res = await mx.call('GET', '/lists/3/contacts');
import os
from mxsin import Mxsin

mx = Mxsin(os.environ['MXSIN_API_KEY'])
res = mx.call('GET', '/lists/3/contacts')

Example response

{
    "contacts": [
        {
            "email": "alex@example.org",
            "status": "subscribed",
            "fields": {
                "name": "Alex"
            },
            "topics": {
                "product-news": true
            },
            "preferences_url": "https://mxsin.com/p/41.abc"
        }
    ],
    "page": 1
}

POST /lists/{id}/contacts

Add or update a contact. Adds a contact or updates an existing one. Addresses on your suppression list are stored but are never mailed. Only add people who asked to receive your mail.

FieldTypeRequiredDescription
emailstringYesContact address
fieldsobjectNoUp to 30 values used as template variables
topicsobjectNo{"topic-name": true|false}
curl -X POST 'https://mxsin.com/api/v1/lists/3/contacts' \
  -H 'Authorization: Bearer mx_YOUR_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "email": "alex@example.org",
    "fields": {
        "name": "Alex"
    },
    "topics": {
        "product-news": true
    }
}'
require 'mxsin.php';
$mx = new Mxsin\Client(getenv('MXSIN_API_KEY'));
$res = $mx->call('POST', '/lists/3/contacts', [
    'email' => 'alex@example.org',
    'fields' => [
        'name' => 'Alex'
    ],
    'topics' => [
        'product-news' => true
    ]
]);
const { Mxsin } = require('./mxsin.js');
const mx = new Mxsin(process.env.MXSIN_API_KEY);
const res = await mx.call('POST', '/lists/3/contacts', {
    "email": "alex@example.org",
    "fields": {
        "name": "Alex"
    },
    "topics": {
        "product-news": true
    }
});
import os
from mxsin import Mxsin

mx = Mxsin(os.environ['MXSIN_API_KEY'])
res = mx.call('POST', '/lists/3/contacts', {
    "email": "alex@example.org",
    "fields": {
        "name": "Alex"
    },
    "topics": {
        "product-news": True
    }
})

Example response

{
    "email": "alex@example.org",
    "status": "subscribed"
}

DELETE /lists/{id}/contacts/{email}

Remove a contact. Deletes the contact from the list. It does not add a suppression.

curl -X DELETE 'https://mxsin.com/api/v1/lists/3/contacts/alex%40example.org' \
  -H 'Authorization: Bearer mx_YOUR_KEY'
require 'mxsin.php';
$mx = new Mxsin\Client(getenv('MXSIN_API_KEY'));
$res = $mx->call('DELETE', '/lists/3/contacts/alex%40example.org');
const { Mxsin } = require('./mxsin.js');
const mx = new Mxsin(process.env.MXSIN_API_KEY);
const res = await mx.call('DELETE', '/lists/3/contacts/alex%40example.org');
import os
from mxsin import Mxsin

mx = Mxsin(os.environ['MXSIN_API_KEY'])
res = mx.call('DELETE', '/lists/3/contacts/alex%40example.org')

Example response

{
    "deleted": true
}

GET /account

Account limits and usage. Your current sending stage, quota, rate and usage in the last 24 hours.

curl -X GET 'https://mxsin.com/api/v1/account' \
  -H 'Authorization: Bearer mx_YOUR_KEY'
require 'mxsin.php';
$mx = new Mxsin\Client(getenv('MXSIN_API_KEY'));
$res = $mx->call('GET', '/account');
const { Mxsin } = require('./mxsin.js');
const mx = new Mxsin(process.env.MXSIN_API_KEY);
const res = await mx.call('GET', '/account');
import os
from mxsin import Mxsin

mx = Mxsin(os.environ['MXSIN_API_KEY'])
res = mx.call('GET', '/account')

Example response

{
    "stage": "production",
    "daily_quota": 1000,
    "per_second": 2,
    "sent_24h": 120,
    "bounce_rate_7d": 0.4,
    "complaint_rate_7d": 0
}

Signed in? The panel’s API docs page fills in your own domain in the examples. Questions: contact us.