API partenaires · v1
Migrations d’e-mails pour les hébergeurs
Quand un client achète un hébergement chez vous, ses e-mails doivent suivre. Avec l’API Migrabox, vous lancez la copie depuis votre panneau de contrôle, ou vous envoyez au client un lien vers une page à votre logo où il saisit lui-même les mots de passe. Nous copions chaque message, dossier et pièce jointe via IMAP ; l’ancienne boîte n’est jamais modifiée.
Cette page est aussi disponible en Italiano · English · Español · Português (Brasil) · Deutsch
Comment cela s’intègre à votre activité
- Même adresse, nouveau serveur. Le cas le plus courant : info@customer.com ne change pas, seul l’hébergement change. Nous copions tout l’historique maintenant, puis les derniers e-mails après le changement de MX, en lisant toujours l’ancien serveur.
- Vous ne manipulez jamais de mots de passe avec les liens de migration : le client les saisit sur migrabox.com ; ils sont chiffrés et supprimés 14 jours après la dernière copie.
- Une boîte, un crédit. Quelle que soit sa taille. Si la première copie ne peut pas démarrer parce qu’un mot de passe est incorrect ou qu’un serveur ne répond pas, le crédit revient sur votre solde.
- Les clés de test sont gratuites et simulent chaque état et chaque webhook : vous pouvez développer toute l’intégration avant d’acheter quoi que ce soit.
Tarifs
Crédit prépayé, valable 12 mois à compter du paiement. Mêmes montants en euros et en dollars américains : vous choisissez la devise au paiement. Plusieurs packs s’additionnent ; le crédit est d’abord pris sur le pack qui expire le premier.
| Pack | Prix | Par boîte |
|---|---|---|
| 20 boîtes | 20,00 € · 20,00 $US | 1,00 € |
| 100 boîtes | 80,00 € · 80,00 $US | 0,80 € |
| 500 boîtes | 300,00 € · 300,00 $US | 0,60 € |
| 2 000 boîtes | 1 000,00 € · 1 000,00 $US | 0,50 € |
Vitesse standard : jusqu’à 20 Mbps par boîte. Une option plus rapide via l’API est prévue (le champ speed est déjà réservé).
Démarrage en 5 minutes
- Créez un compte partenaireLe nom de la société et le pays figurent sur vos reçus. Vous vous connectez avec un lien envoyé par e-mail : aucun mot de passe à gérer.
- Créez une clé de testEspace partenaire → Clés API → mode Test. Elle commence par
mbx_test_et ne s’affiche qu’une seule fois. - Appelez l’APICréez une migration avec l’exemple ci-dessous. En mode test, rien n’est copié : la migration passe par
queued,runningetcompleteden une dizaine de secondes. - Ajoutez un webhookEspace partenaire → Webhooks. Cliquez sur Envoyer un événement de test et vérifiez que votre endpoint contrôle la signature.
- Passez en productionAchetez un pack de crédit, créez une clé
mbx_live_et remplacez la clé de test. Mêmes routes, mêmes 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"Authentification
Chaque requête porte votre clé dans l’en-tête Authorization : Authorization: Bearer mbx_live_…. Les requêtes sans clé valide reçoivent 401 UNAUTHORIZED.
Les clés ne s’affichent qu’une fois et nous n’en conservons qu’une empreinte SHA-256. Renouveler crée une nouvelle clé et laisse l’ancienne active pendant 24 heures ; Révoquer la désactive immédiatement.
Une clé ne voit que les migrations de sa propre société, dans son propre mode : une clé live ne voit jamais les migrations de test, et inversement. Tout le reste répond 404 NOT_FOUND.
Appelez l’API uniquement depuis vos serveurs. Ne mettez jamais une clé dans une page web ou une application mobile.
Mode test
Les clés mbx_test_ utilisent les mêmes routes que les clés live. Elles ne se connectent jamais à un serveur de messagerie, ne stockent jamais de mots de passe et ne consomment jamais de crédit. Vérifications et migrations sont simulées selon une chronologie fixe : 2 secondes en file d’attente, 8 secondes de copie, puis l’état final, avec tous les webhooks qu’enverrait une vraie migration (endpoints de test uniquement).
Choisissez le résultat avec le mot de passe source :
test-auth-failed- La création échoue avec
422 SOURCE_AUTH_FAILED(comme mot de passe de destination :DEST_AUTH_FAILED). test-connect-failed- La création échoue avec
422 SOURCE_CONNECT_FAILED. Un hôte se terminant par.invalidproduit le même effet. test-run-fails- La migration démarre, puis échoue avec
failure_code: AUTH_SOURCEet envoiemigration.failed. test-run-errors- La migration se termine en
completed_with_errors(quelques messages non copiés).
Tout autre mot de passe : la migration aboutit.
Lien de migration ou serveur à serveur
Deux façons de lancer une migration. Les deux aboutissent au même objet migration, aux mêmes webhooks et à la même page de suivi.
Lien de migration (recommandé)
POST /migration-linksrenvoie une URL du typehttps://migrabox.com/en/email-move/#…que vous envoyez à votre client.- La page affiche Migrabox en collaboration avec votre nom et votre logo (à téléverser dans l’espace partenaire).
- Le client saisit les deux mots de passe ; vous pouvez imposer le serveur de destination (votre nouvel hébergement) pour qu’il n’ait à saisir que le nouveau mot de passe.
- Le crédit est débité quand le client valide le formulaire. Vous recevez
link.opened,link.submitted, puis les événements de la migration. - Usage unique, valable de 1 à 30 jours (14 par défaut), révocable tant qu’il n’a pas été utilisé.
Serveur à serveur
POST /migrationsavec les deux boîtes et leurs mots de passe, quand vous les avez déjà (par exemple une boîte que vous êtes en train de créer sur vos serveurs).- Nous vérifions les deux connexions et mesurons la source avant de débiter le crédit : un mot de passe incorrect répond
422et ne coûte rien. - Les mots de passe ne transitent qu’en HTTPS, sont chiffrés au repos (AES-256-GCM), n’apparaissent jamais dans les logs, les réponses ou les webhooks, et sont supprimés 14 jours après la dernière copie.
- Jusqu’à 500 boîtes à la fois avec
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", … }Même adresse, nouvel hébergement
Indiquez same_address: true quand le client garde info@customer.com et la transfère sur vos serveurs. Source et destination partagent l’adresse e-mail ; l’hôte de destination est votre nouveau serveur.
À la création, nous enregistrons les adresses de l’ancien serveur qui a répondu. Chaque exécution suivante s’y connecte (avec TLS vérifié sur l’ancien nom) : une fois les enregistrements MX basculés vers vos serveurs, POST /migrations/{id}/delta lit toujours l’ancienne boîte et récupère ce qui y est arrivé entre-temps.
Parcours recommandé : créez la migration (ou le lien) dès que l’hébergement est actif → la copie s’exécute → changez les MX → appelez delta → terminé. Le delta est gratuit pendant 14 jours après la dernière exécution ; les messages déjà copiés sont ignorés, donc rien n’est dupliqué.
Webhooks
Ajoutez des endpoints dans l’espace partenaire (https uniquement, nom DNS public, jusqu’à 5 par mode). Nous envoyons en POST un événement JSON à chaque endpoint abonné à son type. Les payloads ne contiennent jamais de mots de passe ni de jetons privés.
Répondez 2xx en moins de 10 secondes. Toute autre réponse entraîne de nouvelles tentatives à intervalles croissants (30 s, 2 min, 10 min, 30 min, 1 h, 2 h, 4 h, 6 h, 8 h : environ 22 heures au total). L’espace partenaire affiche chaque livraison et permet de la relancer.
Un événement peut arriver plusieurs fois et dans le désordre : utilisez id pour traiter chaque événement une seule fois, et fiez-vous au status de l’objet.
Événements
migration.created- Une migration a été créée (API, ligne de lot ou lien de migration).
migration.started- Une exécution a commencé la copie (première copie ou delta).
migration.completed- Une exécution s’est terminée :
statusvautcompletedoucompleted_with_errors. migration.failed- Une exécution a échoué : voir
failure_codeetcredit_refunded. migration.cancelled- Une exécution a été arrêtée avec
/cancel. link.opened- Votre client a ouvert un lien de migration pour la première fois.
link.submitted- Votre client a validé le lien de migration : l’objet inclut la nouvelle migration.
credits.low- Le crédit live a atteint votre seuil d’alerte (espace partenaire → Paramètres).
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, …
} }
}Vérifier la signature
Chaque livraison porte l’en-tête Migrabox-Signature: t=<unix seconds>,v1=<hex>. v1 est le HMAC-SHA256 de t + "." + raw body, calculé avec le secret de signature de l’endpoint (whsec_…, affiché une seule fois à la création de l’endpoint).
Calculez-le sur le corps brut (avant de parser le JSON), comparez en temps constant et refusez les horodatages de plus de 5 minutes pour bloquer les rejeux. Pendant le renouvellement d’un secret, l’en-tête peut contenir plusieurs v1 : acceptez l’événement si l’un d’eux correspond.
<?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 afterwardsimport { createHmac, timingSafeEqual } from 'node:crypto';
// rawBody: the request body exactly as received (Buffer or string), before JSON.parse.
export function verifyMigrabox(rawBody, header, secret, toleranceSeconds = 300) {
const parts = header.split(',').map((p) => p.split('='));
const t = Number(parts.find(([k]) => k === 't')?.[1]);
if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
const expected = Buffer.from(createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex'));
return parts.filter(([k]) => k === 'v1').some(([, v]) => {
const got = Buffer.from(v ?? '');
return got.length === expected.length && timingSafeEqual(got, expected);
});
}import hashlib, hmac, time
def verify_migrabox(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
parts = [p.split("=", 1) for p in header.split(",") if "=" in p]
t = next((v for k, v in parts if k == "t"), "")
if not t.isdigit() or abs(time.time() - int(t)) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(expected, v) for k, v in parts if k == "v1")Erreurs
Les erreurs sont du JSON avec un code stable (à utiliser dans votre code) et un message en anglais (pour vos logs). Le statut HTTP indique la catégorie : 4xx, un problème dans votre requête ; 5xx, un problème chez nous (réessayez avec la même Idempotency-Key).
HTTP/1.1 422 Unprocessable Entity
{ "error": { "code": "SOURCE_AUTH_FAILED", "message": "Cannot sign in to the source mailbox" } }| Code | HTTP | Signification |
|---|---|---|
INVALID_INPUT | 400 | Un champ est manquant ou invalide ; le message le nomme (jamais sa valeur). |
UNAUTHORIZED | 401 | Clé absente, mal formée, révoquée ou expirée. |
INSUFFICIENT_CREDITS | 402 | Plus de crédit live (pour un lot : pas assez pour toutes les lignes). |
NOT_FOUND | 404 | Aucun objet de ce type pour cette clé et ce mode. |
RUN_ACTIVE | 409 | Une exécution est déjà en file d’attente ou en cours de copie. |
CREDENTIALS_PURGED | 409 | Plus de 14 jours depuis la dernière exécution : les mots de passe sont supprimés, créez une nouvelle migration. |
IDEMPOTENCY_MISMATCH | 422 | Cette Idempotency-Key a déjà servi pour une requête différente. |
SOURCE_AUTH_FAILED | 422 | Le serveur source a refusé la connexion. DEST_… pour la destination. |
SOURCE_CONNECT_FAILED | 422 | Le serveur source n’a pas répondu (également TLS_FAILED, TIMEOUT, HOST_NOT_ALLOWED). |
SAME_SERVER | 422 | Source et destination sont la même boîte sur le même serveur : refusé, rien n’est copié. |
RATE_LIMITED | 429 | Trop de requêtes pour cette clé : patientez puis réessayez. |
Limites et conventions
- Limites par compte et par mode (toutes vos clés ensemble), par endpoint et par minute : 120 requêtes, 60 créations, 30 vérifications, 10 lots. Au-delà :
429 RATE_LIMITED./probeet/discoveren mode live sont disponibles après votre premier achat de crédit (avant :402 CREDIT_REQUIRED). - Idempotency-Key sur chaque POST : envoyez une valeur unique (votre identifiant de commande ou de boîte). La première réponse est conservée 24 heures et renvoyée à l’identique pour la même clé ; l’en-tête
Idempotent-Replayed: truevous le signale. - Pagination :
GET /migrations?limit=20renvoiehas_moreetnext_cursor; passezcursor=pour la page suivante. Les plus récentes d’abord. - Les noms de serveur doivent être des noms DNS : les adresses IP et les réseaux privés sont refusés. Les certificats TLS sont toujours vérifiés.
- Les dates sont en ISO 8601, en UTC ; le champ
createddes webhooks est en secondes Unix. De nouveaux champs peuvent apparaître à tout moment dans nos réponses : ignorez ceux que vous ne connaissez pas.
Exemples
Exemples complets, sans dépendances. Remplacez la clé par la vôtre.
# 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"<?php
// Works on any PHP 7.4+ with the curl extension (WHMCS, cPanel, Plesk). No dependencies.
function migrabox(string $method, string $path, ?array $body = null, ?string $idempotencyKey = null): array {
$ch = curl_init('https://migrabox.com/api/v1' . $path);
$headers = ['Authorization: Bearer ' . getenv('MIGRABOX_KEY'), 'Accept: application/json'];
if ($body !== null) {
$headers[] = 'Content-Type: application/json';
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
if ($idempotencyKey !== null) $headers[] = 'Idempotency-Key: ' . $idempotencyKey;
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 60,
]);
$raw = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
$json = json_decode((string) $raw, true) ?? [];
if ($status >= 400) throw new RuntimeException(($json['error']['code'] ?? 'HTTP_' . $status) . ': ' . ($json['error']['message'] ?? ''));
return $json;
}
// Hosted link: your customer types the passwords on our page, you never handle them.
$link = migrabox('POST', '/migration-links', [
'locale' => 'en',
'external_id' => 'service-' . $serviceId,
'same_address' => true,
'source_email' => $customerEmail,
'destination' => ['host' => 'imap.yourhosting.com', 'port' => 993, 'security' => 'ssl'],
], 'link-service-' . $serviceId);
echo $link['url']; // send it in your welcome email// Node 18+ (built-in fetch). No dependencies.
const BASE = 'https://migrabox.com/api/v1';
async function migrabox(method, path, body, idempotencyKey) {
const res = await fetch(BASE + path, {
method,
headers: {
Authorization: `Bearer ${process.env.MIGRABOX_KEY}`,
...(body ? { 'Content-Type': 'application/json' } : {}),
...(idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
const json = await res.json();
if (!res.ok) throw Object.assign(new Error(json.error.message), { code: json.error.code, status: res.status });
return json;
}
const migration = await migrabox('POST', '/migrations', {
source: { email: 'info@customer.com', host: 'imap.oldhost.com', password: oldPassword },
destination: { email: 'info@customer.com', host: 'imap.yourhosting.com', password: newPassword },
same_address: true,
external_id: 'order-1001',
}, 'order-1001-mailbox-1');
console.log(migration.id, migration.status); // "queued"# Python 3.8+, standard library only.
import json, os, urllib.request, urllib.error
BASE = "https://migrabox.com/api/v1"
def migrabox(method, path, body=None, idempotency_key=None):
headers = {"Authorization": f"Bearer {os.environ['MIGRABOX_KEY']}"}
data = None
if body is not None:
headers["Content-Type"] = "application/json"
data = json.dumps(body).encode()
if idempotency_key:
headers["Idempotency-Key"] = idempotency_key
req = urllib.request.Request(BASE + path, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as r:
return json.load(r)
except urllib.error.HTTPError as e:
err = json.load(e)["error"]
raise RuntimeError(f"{err['code']}: {err['message']}") from None
batch = migrabox("POST", "/migrations/batch", {"migrations": [
{"source": {"email": m["email"], "host": "imap.oldhost.com", "password": m["old"]},
"destination": {"email": m["email"], "host": "imap.yourhosting.com", "password": m["new"]},
"same_address": True, "external_id": m["id"]}
for m in mailboxes
]}, idempotency_key="import-2026-11-12")
print(batch["id"], batch["status"]) # "processing": then GET /migrations/batch/{id}Référence de l’API
URL de base https://migrabox.com/api/v1. Les schémas complets figurent dans le document OpenAPI.
Account
GET
/v1/accountSolde de crédit et packs, avec leur date d’expiration.
Mailboxes
POST
/v1/discoverParamètres IMAP d’une adresse (le même moteur que le site).
Erreurs:
INVALID_INPUT,CREDIT_REQUIREDPOST
/v1/probeVérifie une connexion via TLS et, avec
measure, compte les messages et les octets.Erreurs:
INVALID_INPUT,CREDIT_REQUIRED,RATE_LIMITED
Migrations
POST
/v1/migrationsCrée et lance une migration avec les deux mots de passe.
Erreurs:
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_MISMATCHPOST
/v1/migrations/batchJusqu’à 500 migrations, traitées en arrière-plan.
Erreurs:
INVALID_INPUT,INSUFFICIENT_CREDITSGET
/v1/migrations/batch/{id}Résultat d’un lot, ligne par ligne.
Erreurs:
NOT_FOUNDGET
/v1/migrationsListe avec filtres (statut, external_id, dates) et curseur.
Erreurs:
INVALID_INPUT,INVALID_CURSORGET
/v1/migrations/{id}Statut et progression.
Erreurs:
NOT_FOUNDPOST
/v1/migrations/{id}/deltaCopie ce qui est arrivé depuis la dernière exécution (après le changement de MX).
Erreurs:
NOT_FOUND,RUN_ACTIVE,CREDENTIALS_PURGED,CREDIT_REFUNDED,NOT_READYPOST
/v1/migrations/{id}/cancelArrête la copie en attente ou en cours.
Erreurs:
NOT_FOUND,NOT_RUNNINGGET
/v1/migrations/{id}/reportToutes les exécutions, avec compteurs et chronologie.
Erreurs:
NOT_FOUND
Hosted links
POST
/v1/migration-linksCrée un lien de migration pour votre client.
Erreurs:
INVALID_INPUT,DESTINATION_REQUIREDGET
/v1/migration-links/{id}Statut d’un lien de migration.
Erreurs:
NOT_FOUNDPOST
/v1/migration-links/{id}/revokeAnnule un lien pas encore utilisé.
Erreurs:
NOT_FOUND,LINK_NOT_OPEN
Historique des versions
- v1 : migrations, lots, liens de migration à votre logo, delta, annulation, rapport, discover, probe, webhooks signés, mode test.