Receiving Resend inbound email without webhooks
Sending an email using the Resend SDK takes only a couple of lines of code. This is to be expected, as the SDK doesn't do much more than wrap a fetch to the POST /emails endpoint.
Last November, Resend shipped a long-requested feature: inbound email[1]. How does the SDK help the user handle inbound email? Same thing as with outbound email: wrap a fetch to the GET /emails/receiving endpoint.
import { Resend } from 'resend';const resend = new Resend('re_xxxx...xxxxxx');const { data } = await resend.emails.receiving.get(emailId);console.log(`Email ${data.id} has been received`);Of course, that doesn't really help. Resend's users want to know when an email has been received. The actual integration point for inbound email is a webhook. The SDK doesn't help with that, at all.
Libraries are inherently limited. We can make them do more if we get rid of the assumption that libraries should only integrate on one side of the stack (usually the backend).
The SDK could help with inbound email by providing a way to register a callback function that will be called when a webhook event is received. Presumably this code path would then also verify the webhook signature and return a 200 status code.
However, this does not actually solve any of the usual friction points of handling incoming webhooks.
To actually be useful, the SDK should be able to define endpoints as part of the user's application. This way, the SDK can handle idempotency and deduplication. To do this, the SDK needs to be able to write to the user's database as well.
Since we're already traversing the stack, let's also bring the frontend in scope. Eventually, the email will have to be shown in the user's application. So it should be possible to retrieve the email from the frontend. This is usually directed through the backend, which then queries the database. If the SDK could also provide the user with frontend reactive hooks, that would truly be useful.
But first, we had to invent the primitives for that: Fragno. Fragno is a toolkit that makes it possible to build full-stack libraries. These libraries are made out of:
- Server-side API routes
- Client-side reactive hooks
- A database schema
The /resend/webhook route is defined as part of the Resend library. This route will be mounted in the user's application.
As is best practice for handling webhooks, the route handler only verifies the signature, stores the event payload, and then immediately returns 200. An async operation is queued to call theonResendWebhook callback, shown on the next tab.
export const registerWebhookRoutes = ({ defineRoute, config, deps }) => [ defineRoute({ method: "POST", path: "/resend/webhook", outputSchema: z.object({ success: z.boolean() }), errorCodes: ["MISSING_SIGNATURE", "WEBHOOK_SIGNATURE_INVALID", "WEBHOOK_ERROR"] as const, handler: async function ({ headers, rawBody }, { json, error }) { if (!config.webhookSecret) { return error({ message: "Missing webhook secret in config", code: "WEBHOOK_ERROR" }, 400); } let event; try { event = deps.resend.webhooks.verify({ payload: rawBody, headers, webhookSecret: config.webhookSecret }); } catch (err) { return error({ message: formatErrorMessage(err), code: "WEBHOOK_SIGNATURE_INVALID" }, 400); } await this.handlerTx() .mutate(({ forSchema }) => { const uow = forSchema(resendSchema); uow.triggerHook("onResendWebhook", { event }); }) .execute(); return json({ success: true }); }, }),];A full-stack library can do much more than just wrap API endpoints like a traditional library. All logic, from ingesting events, handling retries, persisting data, and showing the data in the UI, can be implemented in the library.
But there are even more opportunities for full-stack libraries.
Threads in email clients are not an inherent concept of email. They're emergent behavior based on the headers and metadata of the email. As such, I don't think it makes sense to have threads as a first-class feature of the Resend platform. It's something users can build themselves if need be.
But it might make sense to ship a library that adds an opinionated layer for threading, reply routing, deduplication, and conversation history as first-class concerns.
That is what we did in the implementation of the Resend full-stack library. On the client, the user gets typed hooks: useThreads, useThreadMessages, and the useReplyToThread mutator. These send requests to the user's own backend as needed. The only thing left then is to build the UI on top of these hooks.
import { createResendFragmentClient } from "@fragno-dev/resend-fragment/react";const resend = createResendFragmentClient({ mountRoute: "/api/resend" });function SupportInbox() { const { data: threads } = resend.useThreads(); const { data: messages } = resend.useThreadMessages(threadId); const reply = resend.useReplyToThread(); return ( <ThreadList threads={threads}> <MessageTimeline messages={messages} /> <ReplyBox onSend={(text) => reply.mutate({ path: { threadId }, body: { text }, })} /> </ThreadList> );}As emails are sent and received, email headers are matched and the resolved threads are stored in the database. The end-user develops at a higher abstraction layer, not having to be bothered with the details of the email protocol.
One overlooked aspect of calling third-party APIs is the fact that no one should be calling an API such as Resend synchronously. If we want to send a transactional email to a user that just signed up, we shouldn't be waiting for the email to be sent before we return a success response to the user. Instead, we need to store the intent of sending an email to the database, in the same transaction as processing the user's sign-up.
This is also something traditional libraries don't provide any help with.
The same triggerHook(...) pattern we saw earlier for handling webhooks can be reused to trigger the email sending process in the background. This provides resilience against the provider being down, as well as configuration errors such as a missing API key.
Full-stack libraries are a bit harder to integrate than traditional libraries. Instead of only installing and importing the package, it also needs to be mounted and hooked up to the database. When durable hooks are used, a background process is also required.
This example shows how to integrate the Resend library into an application.
First, a single server instance has to be created. This is where credentials and defaults live, and where the database adapter is configured.
The onEmailReceived callback is the user-facing counterpart of the internal hook we saw earlier.
import { createResendFragment } from "@fragno-dev/resend-fragment";import { databaseAdapter } from "./db";export const resendFragment = createResendFragment( { apiKey: process.env.RESEND_API_KEY!, webhookSecret: process.env.RESEND_WEBHOOK_SECRET!, defaultFrom: "Support <support@example.com>", onEmailReceived: async ({ threadId }) => { console.log("Inbound →", threadId); }, }, { databaseAdapter, mountRoute: "/api/resend" },);The entire process may be handled with a coding agent using the Fragno integration skill.
Agent skill
This skill includes a list of first-party Fragno libraries to make installing them easy. Just ask your agent to integrate the Resend fragment.
npx skills add https://github.com/rejot-dev/fragno --skill fragnoInstall
Or install the package and integrate it manually.
npm install @fragno-dev/resend-fragment @fragno-dev/dbA model for every API provider.
The Resend library is a proof of concept for how any API provider can ship opinionated, full-stack integration libraries.
There are now several “webhookless” Stripe providers/wrappers, presumably because the Stripe API/event surface is now complicated enough to warrant it. The title of this article is a nod to that. Of course, we actually do use webhooks, but this is hidden from the user. We also built a proof of concept for webhookless Stripe: see here.
We did the same for some other API providers, for example: Telegram Bots, and GitHub Apps. More can be found on the “Fragments” page. I'll write more on those later.
In an ideal world, these libraries would be created by the API providers themselves, as they are the foremost experts on their own APIs. They could ship full-stack libraries instead of leaving integrators to figure it out themselves.
Our Fragno Claw-like agent
We're building full-stack libraries to be integrated into our own Claw-like agent. Vertical slices make the maintainability of the entire application easier.
The Resend library lives in the broader Fragno ecosystem, so start with the fragment overview, read the main essay, or jump straight into the docs.
- Resend: Inbound Emails↩
- Durable hooks are persisted background hook executions that automatically retry on failure. They keep webhook-driven work reliable by decoupling intake from processing.↩