Skip to content

ResendApiService — Contacts, Audiences & Templates

ResendApiService — Contacts, Audiences & Templates

ResendApiService (worker/services/resend-api-service.ts) is the Worker’s typed REST wrapper for the Resend Contacts/Audiences and Templates APIs. It handles contact synchronisation and template lifecycle management only.

Email sending is a separate concern — handled by worker/services/email-service.ts.
Changes introduced in: PRs #1714, #1717, #1718, #1719
See also: ZTA Developer Guide


Overview

ResendApiService uses fetch() directly (no additional npm dependency) and validates every request and response with Zod. It is instantiated via createResendApiService(apiKey) wherever contact or template management is needed in the Worker.

Scope

ConcernHandled by
Contact/audience synchronisationResendApiService (this doc)
Template CRUD (create/update/get/list/delete)ResendApiService (this doc)
Transactional email sendingemail-service.tsEmailService

Design principles

  • Constructor-time key validation: the API key is validated against /^re_[A-Za-z0-9_]{8,}$/ in the constructor, failing fast before any network call is attempted.
  • fetch()-based, no SDK dependency: the service calls the Resend REST API directly, keeping the Worker bundle lean.
  • Zod at every trust boundary: request bodies are validated before sending; response bodies are validated before returning.
  • Stable ResendApiError: non-2xx responses throw ResendApiError(status, name, message) — callers can branch on the HTTP status code without parsing error strings.

Architecture — Contact Sync Flow

sequenceDiagram
    participant H as Auth Hook / Route Handler
    participant S as ResendApiService
    participant Z as Zod Schema
    participant R as Resend Contacts API

    H->>S: createResendApiService(env.RESEND_API_KEY)
    Note over S: Constructor validates key format (re_xxxx)
    H->>S: createContact(audienceId, data)
    S->>Z: ResendCreateContactRequestSchema.parse(data)
    Z-->>S: throws Error if invalid
    S->>R: POST /audiences/:id/contacts
    R-->>S: 200 { id } or non-2xx
    S->>Z: ResendCreateContactResponseSchema.parse(json)
    S-->>H: ResendCreateContactResponse or ResendApiError

Constructor — API Key Validation

The service validates the API key format at construction time (fail-fast pattern). This avoids silently sending requests with a misconfigured key.

// worker/services/resend-api-service.ts (simplified)
export class ResendApiService {
private static readonly API_KEY_PATTERN = /^re_[A-Za-z0-9_]{8,}$/;
constructor(private readonly apiKey: string) {
if (!ResendApiService.API_KEY_PATTERN.test(apiKey)) {
throw new Error(
'[ResendApiService] RESEND_API_KEY does not match the expected format (re_xxxxx). ' +
'Verify the secret is set correctly.',
);
}
}
// ...
}
/** Factory — create an instance from the Worker env. */
export function createResendApiService(apiKey: string): ResendApiService {
return new ResendApiService(apiKey);
}

The pattern /^re_[A-Za-z0-9_]{8,}$/ catches obvious misconfiguration (e.g. swapped env var, empty string) without revealing the key value in any error message.


Input Validation — Zod Schemas

All requests are validated with Zod before any network call. The key schemas are:

worker/services/resend-api-service.ts
export const ResendCreateContactRequestSchema = z.object({
email: z.string().email(),
firstName: z.string().optional(),
lastName: z.string().optional(),
unsubscribed: z.boolean().optional(),
});
export const ResendCreateTemplateRequestSchema = z.object({
name: z.string().min(1).max(255),
alias: z.string().max(255).regex(/^[a-z0-9-]+$/).optional(),
subject: z.string().max(998).optional(),
html: z.string().min(1),
text: z.string().optional(),
from: z.string().optional(),
replyTo: z.string().optional(),
});

Validation failures throw an Error before the fetch() call. Response bodies are also validated before being returned, providing a double trust-boundary check.


Templates API

ResendApiService exposes full CRUD for Resend templates:

MethodDescription
createTemplate(data)Create a new template
updateTemplate(id, data)Partial update of an existing template
getTemplate(id)Get a template by ID
listTemplates()List all templates in the account
deleteTemplate(id)Delete a template by ID

Templates are referenced by the stable alias field wherever possible (never by opaque ID), so templates can be recreated without updating callers.


Contacts and Audiences API

ResendApiService wraps the Resend Contacts API to synchronise users with the Resend audience. The service exposes these methods:

MethodDescription
createContact(audienceId, data)Create or upsert a contact in the audience
deleteContact(audienceId, contactIdOrEmail)Delete a contact by ID or email
getContact(audienceId, contactIdOrEmail)Get a contact by ID or email
listContacts(audienceId)List all contacts in the audience

Contact synchronisation typically happens at lifecycle events:

  • On user registrationcreateContact() called (with upsert: true semantics) from the Better Auth onUserCreated hook.
  • On user deletiondeleteContact() called from the account deletion flow.

Contact creation usage

worker/hooks/auth-hooks.ts
onUserCreated: async (user) => {
const resendService = createResendApiService(env.RESEND_API_KEY);
await resendService.createContact(env.RESEND_AUDIENCE_ID, {
email: user.email,
firstName: user.name?.split(' ')[0],
lastName: user.name?.split(' ').slice(1).join(' '),
unsubscribed: false,
});
},

Error Handling

Non-2xx responses from the Resend API throw ResendApiError(status, name, message). Callers can catch and branch on the HTTP status code:

Resend API statusResendApiError.statusTypical name
400400validation_error
401401missing_api_key
403403restricted_api_key
404404not_found
429429rate_limit_exceeded
500500internal_server_error

Request or response validation failures throw a standard Error / ZodError before any network call is made.


Configuration

Environment variableBinding typeRequiredDescription
RESEND_API_KEYWorker SecretYesResend API key (format: re_xxxxx). Must be set via wrangler secret put.
RESEND_AUDIENCE_ID[vars]NoResend audience ID for contact synchronisation. If absent, contact sync is disabled.
Terminal window
wrangler secret put RESEND_API_KEY