Developers

API reference

A small REST API over HTTPS. All endpoints are under /api/v1. There's also a Node SDK and an optional SMTP relay.

Authentication

Send your API key as a Bearer token. Live keys (smk_live_…) deliver; test keys (smk_test_…) validate and record but never deliver.

Authorization: Bearer smk_live_…

Send an email

POST /api/v1/email — requires the From domain to be registered & verified. Returns 202 with { id, status } where status is sent | test | suppressed.

curl -X POST https://sovmail.co.uk/api/v1/email \
  -H "Authorization: Bearer smk_live_…" -H "content-type: application/json" \
  -d '{
    "from": "noreply@mail.your-company.co.uk",
    "to": "user@example.org",
    "subject": "Welcome",
    "text": "Thanks for signing up.",
    "html": "<p>Thanks for signing up.</p>"
  }'

Send with a stored template instead of subject/body:

{ "from": "...", "to": "...", "templateId": "…", "variables": { "name": "Ada" } }

Add "track": true to record delivery status (a delivered status and delivered/deferred events) for a message — handy for confirming sends while testing. It's off by default and overrides your account default; it's delivery status only, never open/click tracking or link rewriting. Bounces and complaints are always recorded regardless.

Code examples

The API is a plain HTTPS POST with a Bearer token, so it drops into any language or framework. Set SOVMAIL_KEY in your environment and use the snippet for your stack.

Node.js (fetch)

const res = await fetch("https://sovmail.co.uk/api/v1/email", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SOVMAIL_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    from: "noreply@mail.your-company.co.uk",
    to: "user@example.org",
    subject: "Welcome",
    text: "Thanks for signing up.",
  }),
});
if (!res.ok) throw new Error(`Sovereign Mail: ${res.status}`);
const { id } = await res.json();

NestJS (@nestjs/axios)

import { Injectable } from "@nestjs/common";
import { HttpService } from "@nestjs/axios";
import { firstValueFrom } from "rxjs";

@Injectable()
export class MailService {
  constructor(private readonly http: HttpService) {}

  async send(to: string, subject: string, text: string) {
    const { data } = await firstValueFrom(
      this.http.post(
        "https://sovmail.co.uk/api/v1/email",
        { from: "noreply@mail.your-company.co.uk", to, subject, text },
        { headers: { Authorization: `Bearer ${process.env.SOVMAIL_KEY}` } },
      ),
    );
    return data; // { id, status }
  }
}

PHP (Guzzle)

$client = new GuzzleHttp\Client();
$res = $client->post('https://sovmail.co.uk/api/v1/email', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('SOVMAIL_KEY')],
    'json' => [
        'from' => 'noreply@mail.your-company.co.uk',
        'to' => 'user@example.org',
        'subject' => 'Welcome',
        'text' => 'Thanks for signing up.',
    ],
]);
$id = json_decode((string) $res->getBody(), true)['id'];

Symfony (HttpClient)

use Symfony\Contracts\HttpClient\HttpClientInterface;

public function send(HttpClientInterface $client): string
{
    $response = $client->request('POST', 'https://sovmail.co.uk/api/v1/email', [
        'auth_bearer' => $_ENV['SOVMAIL_KEY'],
        'json' => [
            'from' => 'noreply@mail.your-company.co.uk',
            'to' => 'user@example.org',
            'subject' => 'Welcome',
            'text' => 'Thanks for signing up.',
        ],
    ]);

    return $response->toArray()['id'];
}

Laravel (HTTP client)

use Illuminate\Support\Facades\Http;

$response = Http::withToken(env('SOVMAIL_KEY'))
    ->post('https://sovmail.co.uk/api/v1/email', [
        'from' => 'noreply@mail.your-company.co.uk',
        'to' => 'user@example.org',
        'subject' => 'Welcome',
        'text' => 'Thanks for signing up.',
    ]);

$id = $response->json('id');

Python (requests)

import os, requests

res = requests.post(
    "https://sovmail.co.uk/api/v1/email",
    headers={"Authorization": f"Bearer {os.environ['SOVMAIL_KEY']}"},
    json={
        "from": "noreply@mail.your-company.co.uk",
        "to": "user@example.org",
        "subject": "Welcome",
        "text": "Thanks for signing up.",
    },
    timeout=15,
)
res.raise_for_status()
message_id = res.json()["id"]

Ruby (Net::HTTP)

require "net/http"
require "json"

uri = URI("https://sovmail.co.uk/api/v1/email")
req = Net::HTTP::Post.new(uri,
  "Authorization" => "Bearer #{ENV['SOVMAIL_KEY']}",
  "Content-Type"  => "application/json")
req.body = {
  from: "noreply@mail.your-company.co.uk",
  to: "user@example.org", subject: "Welcome", text: "Thanks for signing up.",
}.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
id = JSON.parse(res.body)["id"]

Go (net/http)

payload, _ := json.Marshal(map[string]string{
    "from":    "noreply@mail.your-company.co.uk",
    "to":      "user@example.org",
    "subject": "Welcome",
    "text":    "Thanks for signing up.",
})
req, _ := http.NewRequest("POST", "https://sovmail.co.uk/api/v1/email", bytes.NewReader(payload))
req.Header.Set("Authorization", "Bearer "+os.Getenv("SOVMAIL_KEY"))
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)

Batch send

POST /api/v1/email/batch — up to 500 messages; each returns its own result.

{ "messages": [ { "from": "...", "to": "a@x.org", "subject": "Hi", "text": "1" }, … ] }

Templates

GET /api/v1/templates · POST /api/v1/templates — bodies support {{variables}}, {{#if}}/{{#unless}} conditionals, and layouts.

Messages & usage

GET /api/v1/messages — recent sends. Filter with ?status= (e.g. delivered, bounced), ?track=true|false (delivery-tracked messages only), ?q= (recipient/subject) and ?limit= (max 500). Each row includes track. GET /api/v1/usage — plan, monthly limit, used, remaining.

Webhooks

PUT /api/v1/webhook sets a receiver URL and returns a signing secret; we POST events signed with HMAC-SHA256 in X-Sovmail-Signature. We always send message.sent (accepted by the MTA); for messages with delivery tracking on we also send message.delivered when the recipient MTA confirms delivery.

Errors

Standard HTTP codes: 401 bad key · 402 over quota · 403 unverified/suspended/unverified domain · 404 template not found · 422 failed content checks · 429 rate limited. Bodies are { "error": "…" }.