Resend - TundraSoft/tundra-connect GitHub Wiki
Typed Resend email API client for Deno, Bun, Node.js and
Cloudflare Workers. Send single and batch transactional email with idempotency
keys, look up delivery status, reschedule or cancel scheduled email, and verify
Svix-signed webhooks. Requests are validated locally before anything leaves
your process. A lightweight alternative to the official resend SDK for the
endpoints it covers.
| Method | Endpoint |
|---|---|
send(email, { idempotencyKey? }) |
POST /emails |
sendBatch(emails, { idempotencyKey? }) |
POST /emails/batch |
getEmail(id) |
GET /emails/{id} |
rescheduleEmail(id, scheduledAt) |
PATCH /emails/{id} |
cancelEmail(id) |
POST /emails/{id}/cancel |
verifyWebhook({ payload, headers, secret }) |
— (local Svix HMAC check) |
Authentication is a plain Bearer API key (re_...), so no _authInjector
override is needed. Resend rejects any request without a User-Agent with a
403; the client sends tundraconnect-resend unless you configure your own in
headers.
import { Resend } from '@tundraconnect/resend';
const client = new Resend({
auth: { type: 'BEARER', token: 're_123456789' },
});
const { id } = await client.send({
from: 'Acme <[email protected]>',
to: '[email protected]',
subject: 'Welcome to Acme!',
html: '<h1>Welcome!</h1><p>Thanks for signing up.</p>',
});
console.log('sent', id);Your tests don't need to fake HTTP. Have your code take a Resend instance
and pass in a stand-in that returns the shapes from
@tundraconnect/resend/schemas or throws a real ResendError:
import { ResendError } from '@tundraconnect/resend/errors';
const outage = new ResendError('SERVICE_UNAVAILABLE', { status: 503 });
console.log(outage.code);To exercise the client itself against a fake transport, subclass it and
reassign the protected _fetch.
| Topic | Description |
|---|---|
| API | Client configuration and endpoint methods |
| Errors | Error codes and diagnostic metadata |
| Schemas | Public Guardian schemas and inferred types |
Deno:
deno add jsr:@tundraconnect/resendBun:
bunx jsr add @tundraconnect/resendNode.js:
npx jsr add @tundraconnect/resendimport { Resend } from '@tundraconnect/resend';
import { ResendError } from '@tundraconnect/resend/errors';
const client = new Resend({
auth: { type: 'BEARER', token: Deno.env.get('RESEND_API_KEY')! },
});
try {
const { id } = await client.send(
{
from: 'Acme <[email protected]>',
// A bare string works too, for to / cc / bcc / reply_to.
to: ['[email protected]'],
reply_to: '[email protected]',
subject: 'Your invoice',
text: 'Your invoice is attached.',
attachments: [{
content: btoa('invoice body'), // base64, NOT a data: URI
filename: 'invoice.txt',
}],
tags: [{ name: 'invoice_id', value: 'inv_123' }],
},
// Same key + same body within 24h returns the original result instead
// of sending twice — derive it from your record, not a random value.
{ idempotencyKey: 'invoice-inv_123' },
);
const email = await client.getEmail(id);
console.log(email.last_event); // 'sent', 'delivered', 'bounced', ...
} catch (err) {
if (err instanceof ResendError) {
// Branch on the stable code name, never on a message substring.
if (err.code === 'RATE_LIMITED') {
// back off and retry with the same idempotency key
}
console.error(err.code, err.getContextValue('vendorName'));
}
throw err;
}const { data } = await client.sendBatch([
{
from: '[email protected]',
to: '[email protected]',
subject: 'Hi X',
text: 'Hi',
},
{
from: '[email protected]',
to: '[email protected]',
subject: 'Hi Y',
text: 'Hi',
},
]);
console.log(data.map((ref) => ref.id)); // data[i] is emails[i]A batch holds up to 100 emails and is all-or-nothing. Resend does not
support attachments or scheduled_at in a batch, so the client rejects
them locally instead of letting them be silently dropped.
const scheduled = await client.send({
from: '[email protected]',
to: '[email protected]',
subject: 'Reminder',
text: 'Your trial ends tomorrow.',
scheduled_at: 'in 1 hour', // or ISO 8601
});
await client.rescheduleEmail(scheduled.id, '2026-12-24T09:00:00Z');
await client.cancelEmail(scheduled.id);Resend signs webhooks through Svix. Verify with the raw body — the signature breaks on any re-serialization:
Deno.serve(async (req) => {
const event = await client.verifyWebhook({
payload: await req.text(), // text(), never json()
headers: req.headers,
secret: Deno.env.get('RESEND_WEBHOOK_SECRET')!, // whsec_...
});
if (event.type === 'email.bounced') {
console.log('bounced:', event.data.email_id);
}
return new Response(null, { status: 204 });
});| Rule | Raised as |
|---|---|
One of html / text / template
|
REQUEST_VALIDATION_ERROR |
to has 1–50 valid addresses; cc / bcc / reply_to are valid |
REQUEST_VALIDATION_ERROR |
from is email@domain or Name <email@domain>
|
REQUEST_VALIDATION_ERROR |
Tag names/values: 1–256 ASCII letters, digits, _, -
|
REQUEST_VALIDATION_ERROR |
Attachment has exactly one of base64 content / URL path
|
REQUEST_VALIDATION_ERROR |
Template variables: string (≤ 2,000 chars) or safe number; \w{1,50} keys |
REQUEST_VALIDATION_ERROR |
idempotencyKey is 1–256 characters |
REQUEST_VALIDATION_ERROR |
Batch: 1–100 emails, no attachments / scheduled_at
|
REQUEST_VALIDATION_ERROR |
The 40 MB total message size depends on the whole encoded email, so Resend enforces it, not this client.
MIT. Part of Tundra Connect, typed vendor API clients for Deno, Bun, Node.js and Cloudflare Workers.