API para partners · v1
Migraciones de correo para empresas de hosting
Cuando un cliente contrata hosting contigo, su correo también tiene que cambiar de sitio. Con la API de Migrabox lanzas la copia desde tu panel de control, o envías al cliente un enlace a una página con tu logo donde él mismo escribe las contraseñas. Copiamos todos los mensajes, carpetas y adjuntos por IMAP; el buzón antiguo nunca se modifica.
Esta página también está disponible en Italiano · English · Português (Brasil) · Français · Deutsch
Cómo encaja en tu negocio
- Misma dirección, servidor nuevo. El caso más habitual: info@customer.com no cambia, solo cambia el hosting. Copiamos ahora todo el historial y, tras el cambio de MX, los últimos correos, leyendo todavía del servidor antiguo.
- Nunca tocas contraseñas si usas enlaces de migración: el cliente las escribe en migrabox.com, se guardan cifradas y se borran 14 días después de la última copia.
- Un buzón, un crédito. Sea cual sea su tamaño. Si la primera copia no puede empezar porque una contraseña es incorrecta o un servidor no responde, el crédito vuelve a tu saldo.
- Las claves de test son gratuitas y simulan todos los estados y webhooks: puedes construir toda la integración antes de comprar nada.
Precios
Crédito prepago, válido 12 meses desde el pago. Las mismas cifras en euros y en dólares estadounidenses: eliges la moneda al pagar. Los packs se suman; el crédito se descuenta primero del pack que caduca antes.
| Pack | Precio | Por buzón |
|---|---|---|
| 20 buzones | 20,00 € · 20,00 US$ | 1,00 € |
| 100 buzones | 80,00 € · 80,00 US$ | 0,80 € |
| 500 buzones | 300,00 € · 300,00 US$ | 0,60 € |
| 2000 buzones | 1000,00 € · 1000,00 US$ | 0,50 € |
Velocidad estándar: hasta 20 Mbps por buzón. Está prevista una opción más rápida a través de la API (el campo speed ya está reservado).
Empieza en 5 minutos
- Crea una cuenta de partnerEl nombre de la empresa y el país aparecen en tus recibos. Accedes con un enlace que te enviamos por correo: no hay contraseña que gestionar.
- Crea una clave de testÁrea de partners → Claves API → modo Test. Empieza por
mbx_test_y solo se muestra una vez. - Llama a la APICrea una migración con el ejemplo de abajo. En modo test no se copia nada: la migración pasa por
queued,runningycompleteden unos diez segundos. - Añade un webhookÁrea de partners → Webhooks. Pulsa Enviar evento de prueba y comprueba que tu endpoint verifica la firma.
- Pasa a producciónCompra un pack de crédito, crea una clave
mbx_live_y sustituye la de test. Mismas rutas, mismos 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"Autenticación
Cada petición lleva tu clave en la cabecera Authorization: Authorization: Bearer mbx_live_…. Las peticiones sin una clave válida reciben 401 UNAUTHORIZED.
Las claves se muestran una sola vez y solo guardamos su huella SHA-256. Rotar crea una clave nueva y mantiene la antigua activa durante 24 horas; Revocar la desactiva al instante.
Una clave solo ve las migraciones de su propia empresa y de su propio modo: una clave live nunca ve migraciones de test, y al revés. Cualquier otra cosa responde 404 NOT_FOUND.
Usa la API solo desde tus servidores. Nunca pongas una clave en una página web ni en una app móvil.
Modo test
Las claves mbx_test_ usan las mismas rutas que las claves live. Nunca se conectan a un servidor de correo, nunca guardan contraseñas y nunca consumen crédito. Las comprobaciones y las migraciones se simulan con una secuencia fija: 2 segundos en cola, 8 segundos copiando y luego el estado final, con todos los webhooks que enviaría una migración real (solo a endpoints de test).
El resultado lo eliges con la contraseña de origen:
test-auth-failed- La creación falla con
422 SOURCE_AUTH_FAILED(como contraseña de destino:DEST_AUTH_FAILED). test-connect-failed- La creación falla con
422 SOURCE_CONNECT_FAILED. Un host que termine en.invalidhace lo mismo. test-run-fails- La migración empieza y luego falla con
failure_code: AUTH_SOURCEy envíamigration.failed. test-run-errors- La migración termina como
completed_with_errors(algunos mensajes no se copian).
Cualquier otra contraseña: la migración se completa.
Enlace de migración o de servidor a servidor
Hay dos formas de iniciar una migración. Las dos terminan en el mismo objeto de migración, con los mismos webhooks y la misma página de estado.
Enlace de migración (recomendado)
POST /migration-linksdevuelve una URL comohttps://migrabox.com/en/email-move/#…que envías a tu cliente.- La página muestra Migrabox en colaboración con tu nombre y tu logo (súbelo en el área de partners).
- El cliente escribe las dos contraseñas; puedes fijar el servidor de destino (tu nuevo hosting) para que solo tenga que introducir la contraseña nueva.
- El crédito se descuenta cuando el cliente envía el formulario. Recibes
link.opened,link.submittedy después los eventos de la migración. - De un solo uso, válido de 1 a 30 días (14 por defecto); puedes revocarlo antes de que se use.
De servidor a servidor
POST /migrationscon los dos buzones y sus contraseñas, cuando ya las tienes (por ejemplo, un buzón que estás creando en ese momento en tus servidores).- Comprobamos los dos accesos y medimos el origen antes de descontar el crédito: una contraseña incorrecta responde
422y no cuesta nada. - Las contraseñas solo viajan por HTTPS, se guardan cifradas (AES-256-GCM), nunca aparecen en logs, respuestas ni webhooks, y se borran 14 días después de la última copia.
- Hasta 500 buzones a la vez 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", … }Misma dirección, hosting nuevo
Indica same_address: true cuando el cliente conserva info@customer.com y lo traslada a tus servidores. Origen y destino comparten la dirección de correo; el host de destino es tu nuevo servidor.
Al crear la migración guardamos las direcciones del servidor antiguo que respondió. Todas las ejecuciones posteriores se conectan a ellas (con TLS verificado sobre el nombre antiguo), así que, después de apuntar los registros MX a tus servidores, POST /migrations/{id}/delta sigue leyendo el buzón antiguo y recoge lo que haya llegado allí mientras tanto.
Flujo recomendado: crea la migración (o el enlace) en cuanto el hosting esté activo → se hace la copia → cambia los MX → llama a delta → listo. Delta es gratuito durante 14 días desde la última ejecución; los mensajes ya copiados se omiten, así que no se duplica nada.
Webhooks
Añade endpoints en el área de partners (solo https, nombre DNS público, hasta 5 por modo). Enviamos por POST un evento JSON a cada endpoint suscrito a su tipo. Los payloads nunca contienen contraseñas ni tokens privados.
Responde 2xx en 10 segundos como máximo. Cualquier otra respuesta se reintenta con esperas crecientes (30 s, 2 min, 10 min, 30 min, 1 h, 2 h, 4 h, 6 h, 8 h: unas 22 horas en total). El área de partners muestra cada entrega y te permite reintentarla.
Los eventos pueden llegar más de una vez y desordenados: usa id para procesar cada evento una sola vez y toma el status del objeto como referencia.
Eventos
migration.created- Se ha creado una migración (API, fila de un lote o enlace de migración).
migration.started- Una ejecución ha empezado a copiar (primera copia o delta).
migration.completed- Una ejecución ha terminado:
statusescompletedocompleted_with_errors. migration.failed- Una ejecución ha fallado: consulta
failure_codeycredit_refunded. migration.cancelled- Una ejecución se ha detenido con
/cancel. link.opened- Tu cliente ha abierto un enlace de migración por primera vez.
link.submitted- Tu cliente ha enviado el enlace de migración: el objeto incluye la nueva migración.
credits.low- El crédito live ha llegado a tu umbral (área de partners → Ajustes).
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, …
} }
}Verificar la firma
Cada entrega lleva la cabecera Migrabox-Signature: t=<unix seconds>,v1=<hex>. v1 es el HMAC-SHA256 de t + "." + raw body, con el secreto de firma del endpoint como clave (whsec_…, que se muestra una sola vez al crear el endpoint).
Calcúlalo sobre el cuerpo sin procesar (antes de parsear el JSON), compara en tiempo constante y rechaza timestamps de más de 5 minutos para bloquear repeticiones. Durante una rotación del secreto, la cabecera puede llevar más de un v1: acepta el evento si coincide cualquiera de ellos.
<?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")Errores
Los errores son JSON con un code estable (úsalo en tu código) y un message en inglés (para tus logs). El estado HTTP indica el tipo: 4xx, problema en tu petición; 5xx, problema nuestro (reintenta con la misma Idempotency-Key).
HTTP/1.1 422 Unprocessable Entity
{ "error": { "code": "SOURCE_AUTH_FAILED", "message": "Cannot sign in to the source mailbox" } }| Código | HTTP | Significado |
|---|---|---|
INVALID_INPUT | 400 | Falta un campo o no es válido; el mensaje indica cuál (nunca su valor). |
UNAUTHORIZED | 401 | Clave ausente, mal formada, revocada o caducada. |
INSUFFICIENT_CREDITS | 402 | No queda crédito live (en un lote: no basta para todas las filas). |
NOT_FOUND | 404 | No existe ese objeto para esta clave y este modo. |
RUN_ACTIVE | 409 | Ya hay una ejecución en cola o copiando. |
CREDENTIALS_PURGED | 409 | Han pasado más de 14 días desde la última ejecución: las contraseñas se han borrado, crea una migración nueva. |
IDEMPOTENCY_MISMATCH | 422 | Esta Idempotency-Key ya se usó con una petición distinta. |
SOURCE_AUTH_FAILED | 422 | El servidor de origen ha rechazado el acceso. DEST_… para el destino. |
SOURCE_CONNECT_FAILED | 422 | El servidor de origen no ha respondido (también TLS_FAILED, TIMEOUT, HOST_NOT_ALLOWED). |
SAME_SERVER | 422 | Origen y destino son el mismo buzón en el mismo servidor: se rechaza y no se copia nada. |
RATE_LIMITED | 429 | Demasiadas peticiones con esta clave: espera y vuelve a intentarlo. |
Límites y convenciones
- Límites por cuenta y modo (todas tus claves juntas), por endpoint y minuto: 120 peticiones, 60 creaciones, 30 comprobaciones, 10 lotes. Por encima del límite:
429 RATE_LIMITED./probey/discoveren modo live están disponibles tras tu primera compra de crédito (antes:402 CREDIT_REQUIRED). - Idempotency-Key en cada POST: envía un valor único (tu id de pedido o de buzón). La primera respuesta se guarda 24 horas y se repite para la misma clave; la cabecera
Idempotent-Replayed: truete lo indica. - Paginación:
GET /migrations?limit=20devuelvehas_moreynext_cursor; pasacursor=para la página siguiente. Primero las más recientes. - Los nombres de servidor deben ser nombres DNS: se rechazan direcciones IP y redes privadas. Los certificados TLS se verifican siempre.
- Las fechas van en ISO 8601 y UTC; el campo
createdde los webhooks va en segundos Unix. Podemos añadir campos nuevos a nuestras respuestas en cualquier momento: ignora los que no conozcas.
Ejemplos
Ejemplos completos y sin dependencias. Sustituye la clave por la tuya.
# 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}Referencia de la API
URL base https://migrabox.com/api/v1. Los esquemas completos están en el documento OpenAPI.
Account
GET
/v1/accountSaldo de crédito y packs, con su caducidad.
Mailboxes
POST
/v1/discoverConfiguración IMAP de una dirección (el mismo motor que la web).
Errores:
INVALID_INPUT,CREDIT_REQUIREDPOST
/v1/probeComprueba un acceso por TLS y, con
measure, cuenta mensajes y bytes.Errores:
INVALID_INPUT,CREDIT_REQUIRED,RATE_LIMITED
Migrations
POST
/v1/migrationsCrea e inicia una migración con las dos contraseñas.
Errores:
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/batchHasta 500 migraciones, procesadas en segundo plano.
Errores:
INVALID_INPUT,INSUFFICIENT_CREDITSGET
/v1/migrations/batch/{id}Resultado de un lote, fila por fila.
Errores:
NOT_FOUNDGET
/v1/migrationsLista con filtros (estado, external_id, fechas) y cursor.
Errores:
INVALID_INPUT,INVALID_CURSORGET
/v1/migrations/{id}Estado y progreso.
Errores:
NOT_FOUNDPOST
/v1/migrations/{id}/deltaCopia lo que ha llegado desde la última ejecución (tras el cambio de MX).
Errores:
NOT_FOUND,RUN_ACTIVE,CREDENTIALS_PURGED,CREDIT_REFUNDED,NOT_READYPOST
/v1/migrations/{id}/cancelDetiene la copia en cola o en curso.
Errores:
NOT_FOUND,NOT_RUNNINGGET
/v1/migrations/{id}/reportTodas las ejecuciones con contadores y la cronología.
Errores:
NOT_FOUND
Hosted links
POST
/v1/migration-linksCrea un enlace de migración para tu cliente.
Errores:
INVALID_INPUT,DESTINATION_REQUIREDGET
/v1/migration-links/{id}Estado de un enlace de migración.
Errores:
NOT_FOUNDPOST
/v1/migration-links/{id}/revokeAnula un enlace que aún no se ha usado.
Errores:
NOT_FOUND,LINK_NOT_OPEN
Historial de cambios
- v1: migraciones, lotes, enlaces de migración con tu logo, delta, cancelación, informe, discover, probe, webhooks firmados y modo test.