Skip to content
migrabox

Partner API · v1

Email migrations for hosting companies

When a customer buys hosting from you, their email has to move too. With the Migrabox API you start the copy from your control panel, or you send the customer a link to a page with your logo where they type the passwords themselves. We copy every message, folder and attachment over IMAP; the old mailbox is never changed.

This page is also available in Italiano · Español · Português (Brasil) · Français · Deutsch

How it fits your business

  • Same address, new server. The most common case: info@customer.com stays the same, only the hosting changes. We copy the whole history now and the last emails after the MX change, still reading the old server.
  • You never touch passwords if you use hosted links: the customer types them on migrabox.com, they are encrypted and deleted 14 days after the last copy.
  • One mailbox, one credit. Any size. If the first copy cannot start because a password is wrong or a server does not answer, the credit goes back to your balance.
  • Test keys are free and simulate every state and webhook, so you can build the whole integration before buying anything.

Prices

Prepaid credit, valid 12 months from payment. Same figures in euro and US dollars: you choose the currency at checkout. Several packs add up; credit is taken from the pack that expires first.

PackPricePer mailbox
20 mailboxes€20.00 · $20.00$1.00
100 mailboxes€80.00 · $80.00$0.80
500 mailboxes€300.00 · $300.00$0.60
2,000 mailboxes€1,000.00 · $1,000.00$0.50

Standard speed: up to 20 Mbps per mailbox. A faster lane through the API is planned (the speed field is already reserved).

Quickstart in 5 minutes

  1. Create a partner accountCompany name and country go on your receipts. You sign in with a link sent by email: no password to manage.
  2. Create a test keyPartner area → API keys → mode Test. It starts with mbx_test_ and is shown only once.
  3. Call the APICreate a migration with the example below. In test mode nothing is copied: the migration goes through queued, running and completed in about ten seconds.
  4. Add a webhookPartner area → Webhooks. Press Send test event and check your endpoint verifies the signature.
  5. Go liveBuy a credit pack, create a mbx_live_ key, swap it in. Same routes, same payloads.
# 1. Your test key (partner area → API keys). Test mode never copies anything.
export MIGRABOX_KEY=mbx_test_...

# 2. Check the key and your credit
curl https://migrabox.com/api/v1/account -H "Authorization: Bearer $MIGRABOX_KEY"

# 3. Create a migration (server to server)
curl https://migrabox.com/api/v1/migrations \
  -H "Authorization: Bearer $MIGRABOX_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1001-mailbox-1" \
  -d '{
    "source":      { "email": "info@customer.com", "host": "imap.oldhost.com", "password": "old-secret" },
    "destination": { "email": "info@customer.com", "host": "imap.yourhosting.com", "password": "new-secret" },
    "same_address": true,
    "external_id": "order-1001"
  }'

# 4. Follow it (or wait for the migration.completed webhook)
curl https://migrabox.com/api/v1/migrations/MIGRATION_ID -H "Authorization: Bearer $MIGRABOX_KEY"

Authentication

Every request carries your key in the Authorization header: Authorization: Bearer mbx_live_…. Requests without a valid key get 401 UNAUTHORIZED.

Keys are shown once and stored by us only as a SHA-256 fingerprint. Rotate creates a new key and keeps the old one working for 24 hours; Revoke stops it at once.

A key only sees its own company’s migrations, in its own mode: a live key never sees test migrations and vice versa. Anything else answers 404 NOT_FOUND.

Use the API from your servers only. Never put a key in a web page or a mobile app.

Test mode

mbx_test_ keys use the same routes as live keys. They never connect to a mail server, never store passwords and never use credit. Probes and migrations are simulated with a fixed timeline: queued for 2 seconds, then copying for 8 seconds, then the final state, with every webhook a real migration would send (test endpoints only).

Choose the outcome with the source password:

test-auth-failed
Creation fails with 422 SOURCE_AUTH_FAILED (as destination password: DEST_AUTH_FAILED).
test-connect-failed
Creation fails with 422 SOURCE_CONNECT_FAILED. A host ending in .invalid does the same.
test-run-fails
The migration starts, then fails with failure_code: AUTH_SOURCE and sends migration.failed.
test-run-errors
The migration ends as completed_with_errors (a few messages not copied).

Any other password: the migration completes.

Two ways to start a migration. Both end in the same migration object, the same webhooks and the same status page.

Hosted link (recommended)

  • POST /migration-links returns a URL like https://migrabox.com/en/email-move/#… that you send to your customer.
  • The page shows Migrabox in collaboration with your name and logo (upload it in the partner area).
  • The customer types both passwords; you can fix the destination server (your new hosting) so they only enter the new password.
  • The credit is taken when the customer submits. You get link.opened, link.submitted, then the migration events.
  • Single use, valid 1 to 30 days (14 by default), can be revoked before it is used.

Server to server

  • POST /migrations with both mailboxes and their passwords, when you already have them (for example a mailbox you are creating right now on your servers).
  • We check both logins and measure the source before taking the credit: a wrong password answers 422 and costs nothing.
  • Passwords travel only over HTTPS, are encrypted at rest (AES-256-GCM), never appear in logs, responses or webhooks, and are deleted 14 days after the last copy.
  • Up to 500 mailboxes at once with POST /migrations/batch.
curl https://migrabox.com/api/v1/migration-links \
  -H "Authorization: Bearer $MIGRABOX_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: welcome-7731" \
  -d '{
    "locale": "en",
    "external_id": "service-7731",
    "same_address": true,
    "source_email": "info@customer.com",
    "destination": { "host": "imap.yourhosting.com", "port": 993, "security": "ssl" }
  }'
# → { "id": "…", "url": "https://migrabox.com/en/email-move/#…", "status": "open", … }

Same address, new hosting

Set same_address: true when the customer keeps info@customer.com and moves it to your servers. Source and destination share the email address; the destination host is your new server.

At creation we store the addresses of the old server that answered. Every later run connects to them (with TLS verified on the old name), so after you switch the MX records to your servers, POST /migrations/{id}/delta still reads the old mailbox and fetches what arrived there in the meantime.

Recommended flow: create the migration (or the hosted link) as soon as the hosting is active → copy runs → change MX → call delta → done. Delta is free within 14 days of the last run; already copied messages are skipped, so nothing is duplicated.

Webhooks

Add endpoints in the partner area (https only, public DNS name, up to 5 per mode). We POST a JSON event to each endpoint subscribed to its type. Payloads never contain passwords or private tokens.

Answer 2xx within 10 seconds. Anything else is retried with growing delays (30 s, 2 min, 10 min, 30 min, 1 h, 2 h, 4 h, 6 h, 8 h: about 22 hours in all). The partner area shows every delivery and lets you retry it.

Events can arrive more than once and out of order: use id to process each event once and the object’s status as the truth.

Events

migration.created
A migration was created (API, batch row or hosted link).
migration.started
A run started copying (first copy or delta).
migration.completed
A run ended: status is completed or completed_with_errors.
migration.failed
A run failed: see failure_code and credit_refunded.
migration.cancelled
A run was stopped with /cancel.
link.opened
Your customer opened a hosted link for the first time.
link.submitted
Your customer submitted the hosted link: the object includes the new migration.
credits.low
Live credit reached your threshold (partner area → Settings).
POST /your/webhook HTTP/1.1
Content-Type: application/json
Migrabox-Signature: t=1794470900,v1=5f2b…c9
Migrabox-Event-Id: evt_7Qm1…

{
  "id": "evt_7Qm1…",
  "object": "event",
  "api_version": "2026-11-12",
  "type": "migration.completed",
  "created": 1794470900,
  "livemode": true,
  "data": { "object": {
    "id": "92430b5b-…", "object": "migration", "status": "completed", "external_id": "order-1001",
    "source": { "email": "info@customer.com", "host": "imap.oldhost.com" },
    "destination": { "email": "info@customer.com", "host": "imap.yourhosting.com" },
    "progress": { "run": "full", "messages_total": 18234, "messages_done": 18234, "percent": 100, … },
    "credit_refunded": false, "can_delta": true, …
  } }
}

Verify the signature

Each delivery has the header Migrabox-Signature: t=<unix seconds>,v1=<hex>. v1 is the HMAC-SHA256 of t + "." + raw body, keyed with the endpoint’s signing secret (whsec_…, shown once when you create the endpoint).

Compute it on the raw body (before parsing JSON), compare in constant time, and refuse timestamps older than 5 minutes to block replays. During a secret rotation the header may carry more than one v1: accept the event if any of them matches.

<?php
// Verify Migrabox-Signature on the RAW body, before json_decode.
$secret = getenv('MIGRABOX_WEBHOOK_SECRET'); // whsec_...
$body = file_get_contents('php://input');
$header = $_SERVER['HTTP_MIGRABOX_SIGNATURE'] ?? '';
$parts = [];
foreach (explode(',', $header) as $p) { [$k, $v] = array_pad(explode('=', $p, 2), 2, ''); $parts[$k][] = $v; }
$t = (int) ($parts['t'][0] ?? 0);
$expected = hash_hmac('sha256', $t . '.' . $body, $secret);
$valid = abs(time() - $t) <= 300 && array_filter($parts['v1'] ?? [], fn($s) => hash_equals($expected, $s));
if (!$valid) { http_response_code(400); exit; }

$event = json_decode($body, true);
// Deliveries can repeat: use $event['id'] to process each event once.
if ($event['type'] === 'migration.completed') {
    $migration = $event['data']['object']; // ['id'], ['external_id'], ['status'], ['progress'] ...
}
http_response_code(200); // answer within 10 seconds, do slow work afterwards

Errors

Errors are JSON with a stable code (use it in your code) and an English message (for your logs). The HTTP status tells you the class: 4xx your request, 5xx ours (retry with the same Idempotency-Key).

HTTP/1.1 422 Unprocessable Entity
{ "error": { "code": "SOURCE_AUTH_FAILED", "message": "Cannot sign in to the source mailbox" } }
CodeHTTPMeaning
INVALID_INPUT400A field is missing or invalid; the message names it (never its value).
UNAUTHORIZED401Missing, malformed, revoked or expired key.
INSUFFICIENT_CREDITS402No live credit left (for a batch: not enough for all rows).
NOT_FOUND404No such object for this key and mode.
RUN_ACTIVE409A run is already queued or copying.
CREDENTIALS_PURGED409More than 14 days after the last run: passwords are deleted, create a new migration.
IDEMPOTENCY_MISMATCH422This Idempotency-Key was used with a different request.
SOURCE_AUTH_FAILED422The source server refused the login. DEST_… for the destination.
SOURCE_CONNECT_FAILED422The source server did not answer (also TLS_FAILED, TIMEOUT, HOST_NOT_ALLOWED).
SAME_SERVER422Both sides are the same mailbox on the same server: refused, never copied.
RATE_LIMITED429Too many requests for this key: wait and retry.

Limits and conventions

  • Rate limits per account and mode (all your keys together), per endpoint and minute: 120 requests, 60 creations, 30 probes, 10 batches. Over the limit: 429 RATE_LIMITED. Live /probe and /discover are available after your first credit purchase (402 CREDIT_REQUIRED before).
  • Idempotency-Key on every POST: send a unique value (your order or mailbox id). The first answer is stored for 24 hours and replayed for the same key; the header Idempotent-Replayed: true tells you.
  • Pagination: GET /migrations?limit=20 returns has_more and next_cursor; pass cursor= for the next page. Newest first.
  • Server names must be DNS names: IP addresses and private networks are refused. TLS certificates are always verified.
  • Dates are ISO 8601 in UTC; webhook created is Unix seconds. Unknown fields in our answers may be added at any time: ignore them.

Examples

Complete, dependency-free examples. Replace the key with yours.

# 1. Your test key (partner area → API keys). Test mode never copies anything.
export MIGRABOX_KEY=mbx_test_...

# 2. Check the key and your credit
curl https://migrabox.com/api/v1/account -H "Authorization: Bearer $MIGRABOX_KEY"

# 3. Create a migration (server to server)
curl https://migrabox.com/api/v1/migrations \
  -H "Authorization: Bearer $MIGRABOX_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1001-mailbox-1" \
  -d '{
    "source":      { "email": "info@customer.com", "host": "imap.oldhost.com", "password": "old-secret" },
    "destination": { "email": "info@customer.com", "host": "imap.yourhosting.com", "password": "new-secret" },
    "same_address": true,
    "external_id": "order-1001"
  }'

# 4. Follow it (or wait for the migration.completed webhook)
curl https://migrabox.com/api/v1/migrations/MIGRATION_ID -H "Authorization: Bearer $MIGRABOX_KEY"

API reference

Base URL https://migrabox.com/api/v1. The full schemas are in the OpenAPI document.

Account

  • GET/v1/account

    Credit balance and lots with their expiry.

Mailboxes

  • POST/v1/discover

    IMAP settings of an address (same engine as the website).

    Errors: INVALID_INPUT, CREDIT_REQUIRED

  • POST/v1/probe

    Check a login over TLS and, with measure, count messages and bytes.

    Errors: INVALID_INPUT, CREDIT_REQUIRED, RATE_LIMITED

Migrations

  • POST/v1/migrations

    Create and start a migration with both passwords.

    Errors: INVALID_INPUT, INSUFFICIENT_CREDITS, SOURCE_AUTH_FAILED, DEST_AUTH_FAILED, SOURCE_CONNECT_FAILED, DEST_CONNECT_FAILED, SOURCE_TLS_FAILED, SOURCE_HOST_NOT_ALLOWED, SAME_MAILBOX, SAME_SERVER, SAME_ADDRESS_MISMATCH, IDEMPOTENCY_MISMATCH

  • POST/v1/migrations/batch

    Up to 500 migrations, processed in the background.

    Errors: INVALID_INPUT, INSUFFICIENT_CREDITS

  • GET/v1/migrations/batch/{id}

    Outcome of a batch, row by row.

    Errors: NOT_FOUND

  • GET/v1/migrations

    List with filters (status, external_id, dates) and cursor.

    Errors: INVALID_INPUT, INVALID_CURSOR

  • GET/v1/migrations/{id}

    Status and progress.

    Errors: NOT_FOUND

  • POST/v1/migrations/{id}/delta

    Copy what arrived since the last run (after the MX change).

    Errors: NOT_FOUND, RUN_ACTIVE, CREDENTIALS_PURGED, CREDIT_REFUNDED, NOT_READY

  • POST/v1/migrations/{id}/cancel

    Stop the queued or running copy.

    Errors: NOT_FOUND, NOT_RUNNING

  • GET/v1/migrations/{id}/report

    All runs with counters and the timeline.

    Errors: NOT_FOUND

Hosted links

Changelog

  • v1: migrations, batch, hosted links with your logo, delta, cancel, report, discover, probe, signed webhooks, test mode.