Перейти к основному содержимому

Переход с Resend

API устроены похоже, поэтому миграция — это в основном переименование полей и замена обработки ошибок. Ниже — что меняется в коде.

примечание

Соответствия составлены по публичному API Resend на момент написания. Если у вас свежая версия их SDK — сверьтесь с их документацией: поля могли измениться.

Ключи и клиент

ResendMailInfra
Префикс ключаre_…mi_live_… (боевой), mi_test_… (sandbox)
Переменная окруженияRESEND_API_KEYMAILINFRA_API_KEY
База APIhttps://api.resend.comhttps://app.mailinfra.ru/v1
Тестовый режимотдельные тестовые адреса (delivered@resend.dev)отдельный ключ mi_test_…, письмо получает статус SIMULATED
// было
import { Resend } from "resend";
const resend = new Resend(process.env.RESEND_API_KEY);

// стало
import { MailInfra } from "mailinfra";
const client = new MailInfra(); // ключ из MAILINFRA_API_KEY

Отправка письма

Имена полей в основном совпадают:

ResendMailInfraКомментарий
fromfromадрес на подтверждённом домене
to, cc, bccto, cc, bccстрока или массив; всего до 50 адресов
subjectsubjectдо 998 символов
html, texthtml, textнужно хотя бы одно, либо template
replyTo / reply_toreply_toдо 10 адресов
headersheadersсистемные заголовки переопределять нельзя
tags: [{name, value}]tags: ["строка"]разный формат — см. ниже
attachments: [{filename, content}]attachments: [{filename, content}]у нас content — байты, которые SDK кодирует сам; либо готовый content_base64
attachments: [{path}]скачивания по URL нет: приложите содержимое
scheduled_atотложенной отправки нет; планируйте на своей стороне
React-компонент (react)рендерите HTML сами либо используйте серверные шаблоны
// было
const { data, error } = await resend.emails.send({
from: "hello@example.ru",
to: ["user@example.com"],
subject: "Привет",
html: "<p>Привет!</p>",
tags: [{ name: "category", value: "welcome" }],
});
if (error) {
console.error(error);
}

// стало
const result = await client.emails.send({
from: "hello@example.ru",
to: ["user@example.com"],
subject: "Привет",
html: "<p>Привет!</p>",
tags: ["welcome"], // просто строки, до 10, до 32 символов
});

Теги

У Resend тег — пара «имя/значение». У нас это плоские строки (до 10 тегов, до 32 символов каждый), по ним фильтруются письма в дашборде, в emails.list() и в вебхуках. Пары приводите к строке так, как удобно искать: ["category:welcome"] или просто ["welcome"].

Ошибки вместо { data, error }

Главное различие в коде: Resend-SDK возвращает объект с полем error и ничего не бросает. MailInfra бросает типизированные исключения.

// было
const { data, error } = await resend.emails.send({ /* … */ });
if (error) {
if (error.name === "validation_error") { /* … */ }
return;
}
use(data.id);

// стало
import { UnprocessableError, RateLimitError, MailInfraError } from "mailinfra";

try {
const result = await client.emails.send({ /* … */ });
use(result.id);
} catch (error) {
if (error instanceof UnprocessableError) {
for (const field of error.fields) {
console.error(field.field, field.message);
}
} else if (error instanceof RateLimitError) {
// SDK уже подождал Retry-After и повторил; сюда попадаем, если не помогло
} else if (error instanceof MailInfraError) {
console.error(error.code, error.requestId);
} else {
throw error;
}
}

Если хочется сохранить прежний стиль — оберните вызов один раз:

async function send(params) {
try {
return { data: await client.emails.send(params), error: null };
} catch (error) {
return { data: null, error };
}
}

Пакетная отправка

ResendMailInfra
resend.batch.send([...]), до 100 писемclient.emails.sendBatch([...]), до 100 писем

Разница в трактовке ответа: у нас 202 приходит даже при частичном отказе, и у каждого элемента results может быть своё error. Проверяйте их поштучно — подробнее.

Идемпотентность

Resend принимает заголовок Idempotency-Key; у нас он тоже поддерживается, и SDK ставит его сам на каждую отправку. Если вы передавали ключ вручную — просто передайте его вторым аргументом:

await client.emails.send({ /* … */ }, { idempotencyKey: "order_4242_paid" });

Статусы письма

ResendMailInfra
GET /emails/{id}client.emails.get(id)
client.emails.list({...}) — выборка по статусу, тегу, адресату, интервалу
client.emails.events(id) — полная история событий письма
статусы sent, delivered, bounced, …QUEUED, SENDING, SENT, DELIVERED, BOUNCED, COMPLAINED, FAILED, SIMULATED

Вебхуки

Самое заметное отличие — подпись.

ResendMailInfra
ПодписьSvix (svix-id, svix-timestamp, svix-signature)X-MailInfra-Signature: t=…,v1=…, HMAC-SHA256
Проверкабиблиотека svixconstructEvent() из нашего SDK
Тип событияemail.sent, email.delivered, email.bounced, …email.sent, email.delivered, email.bounced, … (совпадает)
Полезная нагрузкаdata с полями письмаdata.email (письмо) + data.event (детали события)
// было
import { Webhook } from "svix";
const wh = new Webhook(process.env.RESEND_WEBHOOK_SECRET);
const event = wh.verify(payload, headers);

// стало
import { constructEvent } from "mailinfra";
const event = await constructEvent({
body: req.body, // сырые байты
secret: process.env.MAILINFRA_WEBHOOK_SECRET,
signatureHeader: req.header("X-MailInfra-Signature"),
});

Домены

Схема та же: добавить домен → положить DNS-записи (SPF/DKIM/DMARC) → дождаться верификации. Записи у нас свои, поэтому на время миграции удобно держать оба провайдера: настройка DNS.

Чего у нас нет

  • отложенной отправки (scheduled_at);
  • рендеринга React-компонентов на стороне API — есть серверные Jinja-шаблоны со слагом и переменными;
  • аудиторий и рассылок по списку контактов (resend.contacts).

Совсем без кода

Если переезжать нужно быстро, а SDK менять не хочется — переключите отправку на SMTP: хост smtp.mailinfra.ru, логин apikey, пароль — ваш ключ. Письма пройдут те же проверки и попадут в те же логи и вебхуки.