Log inSign up

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:

TypeHostValue
TXTmg.example.comv=spf1 include:spf.mailportal.tos.llc ~all
TXTmpYYMM._domainkey.mg.example.comk=rsa; p=… (unique per domain)
CNAMEpm-bounces.mg.example.combounces.mailportal.tos.llc
TXT_dmarc.example.comv=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/withTemplate

Tags, 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).