Щоб перевірити вебхуки Resend локально в Next.js, створіть маршрут App Router POST, який зчитує необроблене тіло, перевірте його заголовки Svix вашим секретом підпису Resend і зареєструйте URL-адресу тунелю HTTPS на інформаційній панелі Resend. Надішліть електронний лист через Resend, а потім обробіть реальні email.sent, email.delivered, email.bounced, або email.complained події на локальному хості.
Що вебхук Resend повідомляє вашій програмі
Відповідь API про те, що електронний лист прийнято, не є доказом того, що він досяг одержувача. Доставка відбувається асинхронно. Веб-хуки Resend дозволяють вашій програмі оновлювати стан повідомлень, пригнічувати неправильні адреси, повідомляти про відмову та реагувати на скарги після завершення оригінального запиту на надсилання. офіційна документація вебхука Resend перераховує типи подій і налаштування інформаційної панелі.
Тест локального вебхуку має охоплювати весь кінцевий автомат, а не лише те, чи досягає POST ваш маршрут. Співвіднесіть ідентифікатор електронної пошти кожної події із записом, створеним під час надсилання. Розглядайте стани як переходи: прийнято, надіслано, доставлено, затримано, відмовлено, поскаржено, відкрито або клацнуто, де це можливо. Пізніший дублікат не повинен перезаписувати більш корисний стан або двічі запускати те саме сповіщення.
Створіть маршрут Next.js App Router
Встановіть верифікатор, який підтримується для формату підпису:
npm install svix
Потім створіть маршрут Node-runtime. Resend підписує оригінальне тіло, тому використовуйте request.text() рівно один раз перед розбором.
// app/api/webhooks/resend/route.ts
import { Webhook } from 'svix';
export const runtime = 'nodejs';
export async function POST(request: Request) {
const payload = await request.text();
const headers = {
'svix-id': request.headers.get('svix-id') ?? '',
'svix-timestamp': request.headers.get('svix-timestamp') ?? '',
'svix-signature': request.headers.get('svix-signature') ?? '',
};
let event: ResendEvent;
try {
const webhook = new Webhook(process.env.RESEND_WEBHOOK_SECRET!);
event = webhook.verify(payload, headers) as ResendEvent;
} catch {
return Response.json({ error: 'Invalid webhook signature' }, { status: 400 });
}
await enqueueResendEvent({
deliveryId: headers['svix-id'],
event,
});
return Response.json({ received: true });
}
Секрет підпису належить цій кінцевій точці вебхуку та зазвичай починається з префікса постачальника. Скопіюйте його з налаштувань вебхуку Resend у ігнорований файл локального середовища, наприклад .env.local. Це не ключ API Resend, який використовується для надсилання електронної пошти.
Чому важливі три заголовки Svix
svix-idунікально ідентифікує доставку та є найкращим ключем ідемпотентності.svix-timestampприв’язує підпис до часу, дозволяючи верифікатору відхиляти застарілі запити за межами допустимого значення.svix-signatureможе містити один або кілька версійних підписів, які використовуються для автентифікації тіла.
Не використовуйте цей протокол, розділяючи рядки заголовка, якщо у вас немає вагомої причини. SDK обробляє кодування, численні підписи та перевірку часових позначок. Resend явно рекомендує використовувати секрет підпису та ці заголовки для перевірки. Чим глибше посібник з перевірки підпису пояснює, чому важливі необроблені байти та безпечні перевірки часу.
Виставте Next.js і зареєструйте кінцеву точку
- бігти
npm run devі підтвердьте, що програма прослуховує порт 3000. - Почніть
npx portpreview 3000в іншому терміналі. - У Resend створіть вебхук, кінцевою точкою якого є
https://YOUR-TUNNEL.portpreview.dev/api/webhooks/resend. - Виберіть лише події електронної пошти, які обробляє ваша програма.
- Скопіюйте секрет підпису кінцевої точки в
RESEND_WEBHOOK_SECRETі перезапустіть Next.js, щоб він завантажив змінну. - Надішліть повідомлення за допомогою підтвердженого домену та перевірте події, які досягають локального маршруту.
Зберігайте загальнодоступну URL-адресу стабільною протягом сеансу. Якщо вихід тунелю змінюється, відредагуйте кінцеву точку Resend перед повторним тестуванням. Кінцева точка, налаштована зі старою URL-адресою, не може досягти вашого нового процесу, навіть якщо сам локальний хост справний.
Використовуйте диспетчер введених подій
Корисні навантаження Webhook мають надходити в один вузький диспетчер. Перевірте обов’язкові поля та зробіть нерозпізнані типи подій видимими, не розглядаючи їх як збої сервера.
async function processEvent(event: ResendEvent) {
switch (event.type) {
case 'email.delivered':
await markDelivered(event.data.email_id, event.created_at);
break;
case 'email.bounced':
await markBounced(event.data.email_id, event.data.bounce?.message);
await suppressIfPermanent(event.data);
break;
case 'email.complained':
await suppressRecipients(event.data.to);
await alertCompliance(event.data.email_id);
break;
default:
await recordUnhandledEvent(event);
}
}
Зберігайте типи корисних даних узгодженими з поточною схемою Resend, а не припускайте, що кожна подія має ідентичні дані. Наприклад, деталі відмови та списки одержувачів можуть бути актуальними лише для певних подій. Збережіть тип події, ідентифікатор електронної пошти постачальника, позначку часу події та мінімальну відредаговану корисну інформацію для розслідувань служби підтримки.
Зробіть обробку ідемпотентною перед повторними спробами тестування
Системи Webhook на практиці забезпечують поведінку доставки принаймні один раз. Тайм-аут може виникнути після фіксації бази даних, але до того, як постачальник отримає вашу відповідь 200. Потім постачальник повторює запит, який ви вже застосували. використання svix-id як унікальний ключ доставки та вставте його в ту саму транзакцію, що й зміна стану.
await db.transaction(async (tx) => {
const inserted = await tx.webhookDelivery.insertOnce({
provider: 'resend',
deliveryId,
});
if (!inserted) return;
await applyEmailEvent(tx, event);
});
Не видаляйте дублікати лише за ідентифікатором електронної пошти, оскільки одна електронна пошта законно отримує кілька типів подій. Залежно від моделі даних зберігайте як унікальний ключ рівня доставки, так і правила переходу між станами. Прочитайте шаблони повторних спроб веб-хука та ідемпотентності перед підключенням подій до виставлення рахунків, припинення або сповіщень клієнтів.
Швидко повертайтеся, не втрачаючи події
Перевірка підпису доречна в шляху запиту; повільна робота бізнесу не є. Збережіть або помістіть перевірену подію в чергу, а потім поверніть 2xx. Якщо ви повернетеся до тривалого запису, збій процесу може втратити подію. Якщо ви чекаєте на кілька віддалених API, ваша кінцева точка може перервати час очікування та запросити повторні спроби. Таблиця вхідних повідомлень бази даних часто є найпростішим локальним і робочим дизайном.
Створення корисних тестових подій
Відправлено та доставлено
Надішліть на адресу, якою ви керуєте, із підтвердженого домену. Запишіть ідентифікатор електронної пошти, повернутий API надсилання, і підтвердьте оновлення вхідних подій у тому самому рядку. Час доставки залежить від сервера одержувача, тому не припускайте, що події надходять негайно або в спрощеній послідовності.
Відскоки
Використовуйте задокументовані тестові адреси або функції тестування Resend, а не придумуйте трафік до непов’язаних доменів. Переконайтеся, що постійні збої пригнічують майбутню пошту, а тимчасові умови відповідають вашій політиці повторних спроб. Не пригнічуйте автоматично кожну відкладену подію.
Скарги
Обробка скарг – це як логіка доставки, так і відповідності. Переконайтеся, що повторний вебхук не створює повторних сповіщень і переконайтеся, що відповідного одержувача виключено з подальших кампаній відповідно до вашої політики.
Усунення несправностей вебхуку Resend
Перевірка підпису завжди не вдається
Переконайтеся, що завантажено секрет підпису кінцевої точки, а не ключ API. використання await request.text(), не аналізуйте та не змінюйте рядки JSON, а передавайте всі три заголовки Svix з їхніми точними значеннями. Після зміни перезапустіть сервер розробки .env.local.
Маршрут повертає 404 або 405
Файли маршруту App Router повинні мати назву route.ts під запланованими сегментами URL-адреси та експортуйте POST. Перевірте, чи проміжне програмне забезпечення переписує запит тунелю на мову або сторінку входу. Перевірте загальнодоступну URL-адресу за допомогою curl і перевірте фактичну відповідь.
Resend показує повторні спроби, незважаючи на успішну обробку
Переконайтеся, що кожне успішне розгалуження швидко повертає 2xx. Помилки, які виникли після оновлення бази даних, можуть призвести до 500 і повторної спроби. Зробіть обробку транзакційною та ідемпотентною, а потім перевірте затримку відповіді.
Події надходять, але не можуть бути пов’язані з електронним листом
Зберігайте ідентифікатор електронної пошти постачальника з вихідної відповіді Resend. Не покладайтеся на рядки теми чи адреси одержувачів як ідентифікатори. Ці поля не є ані унікальними, ані достатньо стабільними для кореляції.
Відтворені знімки не проходять перевірку позначки часу
Це очікується під час повторного відтворення старого підписаного запиту через звичайний верифікатор: його позначка часу може бути за межами дозволеного допуску. Надавати перевагу повторній доставці постачальником, якщо це можливо. Для ізольованих тестів бізнес-логіки перевірте один раз, збережіть дезінфікований пристрій події та перевірте диспетчер окремо. керівництво по відтворенню пояснює цю межу.
Безпека та конфіденційність для тестування подій електронної пошти
- Ніколи не викривайте
RESEND_API_KEYабо секрет підпису кінцевої точки в джерелі, пакетах браузера, знімках екрана або журналах запитів. - Перевірте перед аналізом або збереженням події.
- Видалення одержувачів, тем, заголовків і метаданих повідомлень зі спільних записів тунелю.
- Застосуйте обмеження збереження до необроблених корисних навантажень вебхуку; зберігати лише те, що вимагає підтримки та відповідності.
- Використовуйте окремий локальний секрет кінцевої точки від виробництва та обертайте його, коли тестову кінцеву точку буде видалено.
Остаточний дизайн має працювати однаково після розгортання: загальнодоступна кінцева точка HTTPS, перевірка необробленого тіла, довготривала ідемпотентність, швидке підтвердження та асинхронна обробка стану. Детальну інформацію про необроблений корпус App Router див Next.js посібник з локального хосту webhook.
