API partner · v1
Migrazioni email per hosting provider
Quando un cliente compra un hosting da te, anche le sue email devono traslocare. Con l’API Migrabox avvii la copia dal tuo pannello, oppure mandi al cliente un link a una pagina con il tuo logo dove le password le inserisce lui. Copiamo via IMAP ogni messaggio, cartella e allegato; la vecchia casella non viene mai modificata.
Questa pagina è disponibile anche in English · Español · Português (Brasil) · Français · Deutsch
Come si inserisce nel tuo lavoro
- Stesso indirizzo, nuovo server. È il caso più comune: info@cliente.it resta uguale, cambia solo l’hosting. Copiamo subito tutto lo storico e, dopo il cambio dei record MX, le ultime email, continuando a leggere il vecchio server.
- Non tocchi mai le password se usi i link ospitati: il cliente le inserisce su migrabox.com, vengono cifrate e cancellate 14 giorni dopo l’ultima copia.
- Una casella, un credito. Qualsiasi dimensione. Se la prima copia non parte perché una password è sbagliata o un server non risponde, il credito torna nel tuo saldo.
- Le chiavi di test sono gratuite e simulano ogni stato e ogni webhook: costruisci tutta l’integrazione prima di comprare qualcosa.
Prezzi
Credito prepagato, valido 12 mesi dal pagamento. Stesse cifre in euro e in dollari USA: scegli la valuta al pagamento. Più pacchetti si sommano; il credito si consuma dal pacchetto che scade prima. Prezzi IVA inclusa.
| Pacchetto | Prezzo | A casella |
|---|---|---|
| 20 caselle | 20,00 € · 20,00 USD | 1,00 € |
| 100 caselle | 80,00 € · 80,00 USD | 0,80 € |
| 500 caselle | 300,00 € · 300,00 USD | 0,60 € |
| 2.000 caselle | 1000,00 € · 1000,00 USD | 0,50 € |
Velocità standard: fino a 20 Mbps per casella. Una corsia più veloce via API è prevista (il campo speed è già riservato).
Partire in 5 minuti
- Crea un account partnerRagione sociale, paese e partita IVA vanno sulle ricevute. Accedi con un link che ti mandiamo via email: nessuna password da gestire.
- Crea una chiave di testArea partner → Chiavi API → modalità Test. Inizia con
mbx_test_e la vedi una sola volta. - Chiama l’APICrea una migrazione con l’esempio qui sotto. In test non viene copiato niente: la migrazione passa per
queued,runningecompletedin una decina di secondi. - Aggiungi un webhookArea partner → Webhook. Premi Invia evento di prova e controlla che il tuo endpoint verifichi la firma.
- Vai in produzioneAcquista un pacchetto di credito, crea una chiave
mbx_live_e sostituiscila. Stesse rotte, stessi payload.
# 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"Autenticazione
Ogni richiesta porta la chiave nell’header Authorization: Authorization: Bearer mbx_live_…. Senza una chiave valida la risposta è 401 UNAUTHORIZED.
Le chiavi si vedono una sola volta e noi ne conserviamo solo l’impronta SHA-256. Ruota crea una chiave nuova e lascia funzionare la vecchia per 24 ore; Revoca la blocca subito.
Una chiave vede solo le migrazioni della propria azienda e della propria modalità: una chiave live non vede mai le migrazioni di test e viceversa. Tutto il resto risponde 404 NOT_FOUND.
Usa l’API solo dai tuoi server. Non mettere mai una chiave in una pagina web o in un’app.
Modalità test
Le chiavi mbx_test_ usano le stesse rotte delle chiavi live. Non si collegano mai a un server di posta, non conservano password e non consumano credito. Verifiche e migrazioni sono simulate con una sequenza fissa: 2 secondi in coda, 8 secondi di copia, poi lo stato finale, con tutti i webhook che manderebbe una migrazione vera (solo agli endpoint di test).
Scegli l’esito con la password della casella di origine:
test-auth-failed- La creazione fallisce con
422 SOURCE_AUTH_FAILED(come password di destinazione:DEST_AUTH_FAILED). test-connect-failed- La creazione fallisce con
422 SOURCE_CONNECT_FAILED. Lo stesso con un host che finisce in.invalid. test-run-fails- La migrazione parte, poi fallisce con
failure_code: AUTH_SOURCEe mandamigration.failed. test-run-errors- La migrazione termina come
completed_with_errors(alcuni messaggi non copiati).
Qualsiasi altra password: la migrazione si completa.
Link ospitato o da server a server
Due modi per avviare una migrazione. Entrambi producono lo stesso oggetto migrazione, gli stessi webhook e la stessa pagina di stato.
Link ospitato (consigliato)
POST /migration-linksrestituisce un URL comehttps://migrabox.com/it/trasloco-email/#…da mandare al cliente.- La pagina mostra Migrabox in collaborazione con il tuo nome e il tuo logo (lo carichi nell’area partner).
- Il cliente inserisce le due password; puoi fissare il server di destinazione (il tuo nuovo hosting), così deve scrivere solo la nuova password.
- Il credito si scala quando il cliente invia il modulo. Ricevi
link.opened,link.submittede poi gli eventi della migrazione. - Si usa una volta sola, vale da 1 a 30 giorni (14 di default) e puoi revocarlo finché non è stato usato.
Da server a server
POST /migrationscon le due caselle e le loro password, quando le hai già (per esempio una casella che stai creando adesso sui tuoi server).- Verifichiamo i due accessi e misuriamo l’origine prima di scalare il credito: una password sbagliata risponde
422e non costa niente. - Le password viaggiano solo in HTTPS, sono cifrate a riposo (AES-256-GCM), non compaiono mai in log, risposte o webhook e vengono cancellate 14 giorni dopo l’ultima copia.
- Fino a 500 caselle alla volta con
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", … }Stesso indirizzo, nuovo hosting
Imposta same_address: true quando il cliente tiene info@cliente.it e lo sposta sui tuoi server. Origine e destinazione hanno lo stesso indirizzo; l’host di destinazione è il tuo nuovo server.
Alla creazione memorizziamo gli indirizzi del vecchio server che ha risposto. Ogni copia successiva si collega lì (con TLS verificato sul vecchio nome), così dopo che hai spostato i record MX sui tuoi server, POST /migrations/{id}/delta legge ancora la vecchia casella e recupera quello che è arrivato nel frattempo.
Flusso consigliato: crea la migrazione (o il link ospitato) appena l’hosting è attivo → parte la copia → cambi i record MX → chiami delta → fatto. Il delta è gratuito entro 14 giorni dall’ultima copia; i messaggi già copiati vengono saltati, quindi niente doppioni.
Webhook
Aggiungi gli endpoint nell’area partner (solo https, nome DNS pubblico, fino a 5 per modalità). Mandiamo un evento JSON in POST a ogni endpoint iscritto a quel tipo. I payload non contengono mai password o token privati.
Rispondi 2xx entro 10 secondi. Qualsiasi altra risposta viene riprovata con attese crescenti (30 s, 2 min, 10 min, 30 min, 1 h, 2 h, 4 h, 6 h, 8 h: circa 22 ore in tutto). L’area partner mostra ogni consegna e ti fa riprovarla.
Un evento può arrivare più di una volta e fuori ordine: usa id per elaborarlo una volta sola e lo status dell’oggetto come fonte di verità.
Eventi
migration.created- È stata creata una migrazione (API, riga di un batch o link ospitato).
migration.started- Una copia è partita (prima copia o delta).
migration.completed- Una copia è terminata:
statusècompletedocompleted_with_errors. migration.failed- Una copia non è riuscita: vedi
failure_codeecredit_refunded. migration.cancelled- Una copia è stata fermata con
/cancel. link.opened- Il cliente ha aperto per la prima volta un link ospitato.
link.submitted- Il cliente ha inviato il link ospitato: l’oggetto contiene la nuova migrazione.
credits.low- Il credito live ha raggiunto la tua soglia (area partner → Impostazioni).
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, …
} }
}Verifica la firma
Ogni consegna ha l’header Migrabox-Signature: t=<secondi unix>,v1=<hex>. v1 è l’HMAC-SHA256 di t + "." + corpo grezzo, con chiave il segreto di firma dell’endpoint (whsec_…, mostrato una sola volta alla creazione).
Calcolalo sul corpo grezzo (prima del parse JSON), confronta in tempo costante e rifiuta i timestamp più vecchi di 5 minuti per bloccare le ripetizioni. Durante la rotazione del segreto l’header può contenere più v1: accetta l’evento se uno qualsiasi corrisponde.
<?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")Errori
Gli errori sono JSON con un code stabile (da usare nel tuo codice) e un message in inglese (per i log). Lo status HTTP indica la classe: 4xx la tua richiesta, 5xx noi (riprova con la stessa Idempotency-Key).
HTTP/1.1 422 Unprocessable Entity
{ "error": { "code": "SOURCE_AUTH_FAILED", "message": "Cannot sign in to the source mailbox" } }| Codice | HTTP | Significato |
|---|---|---|
INVALID_INPUT | 400 | Un campo manca o non è valido; il messaggio lo nomina (mai il suo valore). |
UNAUTHORIZED | 401 | Chiave mancante, malformata, revocata o scaduta. |
INSUFFICIENT_CREDITS | 402 | Credito live esaurito (per un batch: non basta per tutte le righe). |
NOT_FOUND | 404 | Nessun oggetto con quell’id per questa chiave e modalità. |
RUN_ACTIVE | 409 | C’è già una copia in coda o in corso. |
CREDENTIALS_PURGED | 409 | Sono passati più di 14 giorni dall’ultima copia: le password sono cancellate, crea una nuova migrazione. |
IDEMPOTENCY_MISMATCH | 422 | Questa Idempotency-Key è stata usata con una richiesta diversa. |
SOURCE_AUTH_FAILED | 422 | Il server di origine ha rifiutato l’accesso. DEST_… per la destinazione. |
SOURCE_CONNECT_FAILED | 422 | Il server di origine non risponde (anche TLS_FAILED, TIMEOUT, HOST_NOT_ALLOWED). |
SAME_SERVER | 422 | Le due parti sono la stessa casella sullo stesso server: rifiutato, mai copiato. |
RATE_LIMITED | 429 | Troppe richieste per questa chiave: aspetta e riprova. |
Limiti e convenzioni
- Limiti per account e modalità (tutte le tue chiavi insieme), per endpoint e per minuto: 120 richieste, 60 creazioni, 30 verifiche, 10 batch. Oltre il limite:
429 RATE_LIMITED./probee/discoverlive sono disponibili dopo il primo acquisto di credito (prima:402 CREDIT_REQUIRED). - Idempotency-Key su ogni POST: manda un valore univoco (l’id del tuo ordine o della casella). La prima risposta resta salvata 24 ore e viene ripetuta per la stessa chiave; te lo dice l’header
Idempotent-Replayed: true. - Paginazione:
GET /migrations?limit=20restituiscehas_moreenext_cursor; passacursor=per la pagina successiva. Dalla più recente. - I nomi dei server devono essere nomi DNS: indirizzi IP e reti private vengono rifiutati. I certificati TLS sono sempre verificati.
- Le date sono ISO 8601 in UTC; il
createddei webhook è in secondi Unix. Possiamo aggiungere campi alle risposte in qualsiasi momento: ignora quelli che non conosci.
Esempi
Esempi completi, senza dipendenze. Sostituisci la chiave con la tua.
# 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}Riferimento API
URL di base https://migrabox.com/api/v1. Gli schemi completi sono nel documento OpenAPI.
Account
GET
/v1/accountSaldo del credito e lotti con le scadenze.
Mailboxes
POST
/v1/discoverImpostazioni IMAP di un indirizzo (stesso motore del sito).
Errori:
INVALID_INPUT,CREDIT_REQUIREDPOST
/v1/probeVerifica un accesso in TLS e, con
measure, conta messaggi e byte.Errori:
INVALID_INPUT,CREDIT_REQUIRED,RATE_LIMITED
Migrations
POST
/v1/migrationsCrea e avvia una migrazione con le due password.
Errori:
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/batchFino a 500 migrazioni, elaborate in background.
Errori:
INVALID_INPUT,INSUFFICIENT_CREDITSGET
/v1/migrations/batch/{id}Esito di un batch, riga per riga.
Errori:
NOT_FOUNDGET
/v1/migrationsElenco con filtri (stato, external_id, date) e cursore.
Errori:
INVALID_INPUT,INVALID_CURSORGET
/v1/migrations/{id}Stato e avanzamento.
Errori:
NOT_FOUNDPOST
/v1/migrations/{id}/deltaCopia ciò che è arrivato dall’ultima copia (dopo il cambio MX).
Errori:
NOT_FOUND,RUN_ACTIVE,CREDENTIALS_PURGED,CREDIT_REFUNDED,NOT_READYPOST
/v1/migrations/{id}/cancelFerma la copia in coda o in corso.
Errori:
NOT_FOUND,NOT_RUNNINGGET
/v1/migrations/{id}/reportTutte le copie con i contatori e la cronologia.
Errori:
NOT_FOUND
Hosted links
POST
/v1/migration-linksCrea un link ospitato per il cliente.
Errori:
INVALID_INPUT,DESTINATION_REQUIREDGET
/v1/migration-links/{id}Stato di un link ospitato.
Errori:
NOT_FOUNDPOST
/v1/migration-links/{id}/revokeAnnulla un link non ancora usato.
Errori:
NOT_FOUND,LINK_NOT_OPEN
Novità
- v1: migrazioni, batch, link ospitati con il tuo logo, delta, interruzione, report, discover, probe, webhook firmati, modalità test.