Mailportal API
Base URL https://mailportal.tos.llc. Two dialects on one service: Mailgun's /v3 API and Postmark's API. Pick whichever your code already speaks.
1. Add a domain and publish DNS
Add a sending domain in the dashboard. A subdomain such as mg.example.com is recommended. Publish the records shown:
| Type | Host | Value |
|---|---|---|
| TXT | mg.example.com | v=spf1 include:spf.mailportal.tos.llc ~all |
| TXT | mpYYMM._domainkey.mg.example.com | k=rsa; p=… (unique per domain) |
| CNAME | pm-bounces.mg.example.com | bounces.mailportal.tos.llc |
| TXT | _dmarc.example.com | v=DMARC1; p=none; rua=mailto:dmarc@example.com |
SPF + DKIM are required. Domains are re-checked automatically; one that loses its records stops sending.
2. Create an API key
Keys look like key-…. An account key works for every domain; a sending key is limited to one. The same key authenticates both APIs and SMTP.
Mailgun-style API
HTTP Basic auth with user api and your key. multipart/form-data or form-urlencoded.
curl -s --user 'api:key-xxxxxxxx' https://mailportal.tos.llc/v3/mg.example.com/messages \
-F from='Shop <orders@mg.example.com>' \
-F to='alice@example.org' -F to='Bob <bob@example.org>' \
-F subject='Your order #1042' \
-F text='Thanks for your order!' \
-F html='<p>Thanks for your <b>order</b>!</p>' \
-F o:tag='receipts' -F v:order_id='1042' \
-F attachment=@invoice.pdf
{"id": "<5b0c…@mg.example.com>", "message": "Queued. Thank you."}
Supported: from to cc bcc subject text html attachment inline h:X-Header v:var o:tag o:testmode o:tracking o:tracking-opens o:tracking-clicks template t:variables recipient-variables (batch: %recipient.name%), %unsubscribe_url%. Raw MIME: POST /v3/{domain}/messages.mime with to + message.
Also: GET /v3/domains, GET /v3/domains/{d}, PUT /v3/domains/{d}/verify, GET /v3/{d}/events, GET /v3/{d}/stats/total, /v3/{d}/bounces|unsubscribes|complaints, /v3/domains/{d}/webhooks, /v3/{d}/templates, GET /v3/{d}/tags.
Postmark-style API
Header X-Postmark-Server-Token: key-…, JSON body. The sending domain is picked from the From address.
curl -s https://mailportal.tos.llc/email \
-H 'Accept: application/json' -H 'Content-Type: application/json' \
-H 'X-Postmark-Server-Token: key-xxxxxxxx' \
-d '{"From":"orders@mg.example.com","To":"alice@example.org","Subject":"Your order",
"HtmlBody":"<p>Thanks!</p>","TextBody":"Thanks!","Tag":"receipts",
"Metadata":{"order_id":"1042"},"TrackOpens":true,"MessageStream":"outbound"}'
{"To":"alice@example.org","SubmittedAt":"2026-11-14T10:00:00.0000000Z","MessageID":"8e1c…","ErrorCode":0,"Message":"OK"}
Endpoints: POST /email, /email/batch (≤500), /email/withTemplate, /email/batchWithTemplates, GET /messages/outbound, /messages/outbound/{id}/details, GET /bounces, PUT /bounces/{id}/activate, /deliverystats, /stats/outbound, /templates, /webhooks, /message-streams/{stream}/suppressions[/dump|/delete], /server. Error codes follow Postmark: 10 bad token, 300 invalid request, 406 inactive recipient, 412 sandbox restriction.
SMTP relay
- Host
- smtp.mailportal.tos.llc
- Port
- 2525 with STARTTLS, or 2465 with implicit TLS
- Username
- SMTP login from the domain page (e.g.
postmaster@mg.example.com), or an API key - Password
- its password, or the same API key
Optional headers (stripped before delivery): X-Mailgun-Tag, X-Mailgun-Variables (JSON), X-Mailgun-Track-Opens, X-PM-Tag, X-PM-Message-Stream, X-PM-Metadata-*, X-PM-TrackOpens.
# Laravel .env
MAIL_MAILER=smtp
MAIL_HOST=smtp.mailportal.tos.llc
MAIL_PORT=2525
MAIL_ENCRYPTION=tls
MAIL_USERNAME=postmaster@mg.example.com
MAIL_PASSWORD=********Using existing SDKs
// Node — mailgun.js
const mg = new Mailgun(FormData).client({ username: 'api', key: process.env.KEY, url: 'https://mailportal.tos.llc' });
await mg.messages.create('mg.example.com', { from, to, subject, text });
// Node — postmark
const client = new postmark.ServerClient(process.env.KEY, { requestHost: 'mailportal.tos.llc' });
await client.sendEmail({ From, To, Subject, TextBody });
// PHP — mailgun/mailgun-php
$mg = Mailgun\Mailgun::create($key, 'https://mailportal.tos.llc');
// Laravel mailgun driver — config/services.php
'mailgun' => ['domain' => 'mg.example.com', 'secret' => env('KEY'), 'endpoint' => 'mailportal.tos.llc'],Templates
Create templates in the dashboard or via API. Mustache-style: {{name}} (HTML-escaped), {{{html}}} (raw), {{#items}}…{{/items}}, {{^empty}}…{{/empty}}, {{#each items}}{{this.title}}{{/each}}.
-F template='password-reset' -F t:variables='{"name":"Ada","link":"https://…"}' # Mailgun
{"TemplateAlias":"password-reset","TemplateModel":{"name":"Ada"},"From":"…","To":"…"} # Postmark /email/withTemplateTags, metadata, tracking, streams
Tags (max 10) and metadata are echoed in every event and webhook. Open tracking adds a 1×1 pixel; click tracking rewrites http(s) links through signed redirects. Mail on the broadcast stream always carries RFC 8058 one-click List-Unsubscribe. Use %unsubscribe_url% or {{{pm:unsubscribe}}} in your content for a visible link.
Events & logs
Event types: accepted delivered failed (severity permanent/temporary, reason bounce / espblock / generic / old / suppress-*), opened clicked unsubscribed complained. Failed deliveries are retried for 24 hours.
curl -s --user 'api:key-…' 'https://mailportal.tos.llc/v3/mg.example.com/events?event=failed&limit=50'Webhooks
Mailgun-format payloads carry a signature. Verify it before trusting the body:
// Node
const ok = crypto.createHmac('sha256', SIGNING_KEY)
.update(body.signature.timestamp + body.signature.token).digest('hex') === body.signature.signature;
Return 2xx to acknowledge. Failures are retried with backoff for about 16 hours; return 406 to stop retries. Postmark-format hooks send RecordType = Delivery / Bounce / Open / Click / SpamComplaint / SubscriptionChange.
Suppressions
Hard bounces (5.1.x "no such user"), spam complaints and unsubscribes are added automatically, per domain. Policy blocks (5.7.x) are not suppressed. Sending to a suppressed address is logged as failed / suppress-bounce; Postmark-style sends where every recipient is inactive return error 406.
Limits & sandbox
New accounts are in sandbox: they can send to their own address plus up to 5 authorized recipients, 50 per day. After review, accounts get hourly and daily limits that grow with clean history. Accounts whose 24-hour hard-bounce rate passes 8% or whose complaint rate passes 0.3% are suspended automatically and their queued mail is held. Max message size 26214400 bytes, max 1000 recipients per message (50 on the Postmark API).