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

Переход с 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

На других языках клиент создаётся так же — ключ берётся из MAILINFRA_API_KEY:

from mailinfra import MailInfra
client = MailInfra()
use MailInfra\MailInfra;
$client = new MailInfra();
client, err := mailinfra.New()

Дальше примеры — на TypeScript. Набор методов во всех четырёх SDK один, а имена полей письма следуют языку: в Node — как в API (from, reply_to), в PHP — именованные аргументы в camelCase (from:, replyTo:), в Python — именованные аргументы (from_email=, потому что from — ключевое слово), в Go — поля структуры SendParams (From, FromName, To, CC). Примеры на каждом языке — на страницах Python, Node, PHP и Go.

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

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

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_atsend_atISO 8601 с часовым поясом, до 7 дней вперёд; естественный язык («in 1 hour») не принимается. Отмена — POST /v1/emails/{id}/cancel, перенос — PATCH /v1/emails/{id}, как emails.cancel и emails.update у Resend
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 тег — пара «имя/значение». У нас для этого два поля:

  • metadata — пары «ключ — значение» (до 20): {"order_id": "1042"}. Они возвращаются в письме и в каждом вебхуке — ближе всего к тегам Resend;
  • tags — плоские строки (до 10, до 32 символов каждая), по ним фильтруются письма в дашборде, в emails.list() и в статистике: ["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.

Чего у нас нет​

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

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

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