Skip to content
Cowliss is in beta: features and pricing may change.

Moving to the Cowliss SDK

The facade exists so you can start without touching your sending code. This page is the other end: what adopting @cowliss/sdk actually changes, and what it does not.

The short version: both calls reach the same send service, so everything around the message (your domain, your suppression list, your delivery rows, your credit) is unaffected. What you gain is the rest of Cowliss, which the Resend shape has nowhere to express.

// The facade: Resend's SDK, Resend's field names.
await resend.emails.send({
from: "Acme <[email protected]>",
reply_to: "[email protected]",
template: { id: "receipt", variables: { orderId: "A-2291" } },
});
// → { id: "dlv_…" }
// Cowliss: camelCase, and the app comes from the client.
const { deliveryId } = await cow.emails.send({
from: "Acme <[email protected]>",
replyTo: "[email protected]",
template: { key: "receipt", props: { orderId: "A-2291" } },
});
// → { deliveryId: "dlv_…" }
The facade Cowliss Note
The app id in RESEND_BASE_URL sourceId on the client The Resend SDK has no field for it, so the facade puts it in the path. Same pipe either way.
reply_to replyTo Every field is camelCase.
template: { id, variables } template: { key, props } The same pushed template, renamed to what the rest of Cowliss calls it.
to as an address or an array Unchanged Both take one address or up to 50.
tags Not taken Accepted and dropped by the facade; Cowliss tags a delivery by its own fields.
{ id } { deliveryId } The same id, which is the delivery row.
{ statusCode, message, name } { error: { code, message } } Cowliss errors follow the shared envelope.

The full field list is on emails.send.

  • Automations. Resend’s shape has one verb, send now. No wait, no branch, no enrollment, and no cancelling a message because the person did the thing in the meantime. That is the whole of automations.
  • Templates by key only. The facade names a pushed template and passes variables. It has no way to preview one, test one, or read what it rendered.
  • Idempotency-Key on a batch. A single send honours the header and replays the first successful answer; a failed send frees its key. A batch ignores it: a retried batch is a second batch.
  • A profile. The facade takes addresses only, and an address carries no consent and no frequency cap, which is why a template its recipients can turn off is refused when it goes to one. cow.emails.send can address a profile (to: { userId: "shop_1" }), checks its consent and carries the unsubscribe links. Resend’s shape has nowhere to put one.
  • attachments, scheduled_at, topic_id and the domains, api-keys, audiences, contacts and broadcasts resources are not served at all.

Both paths call the same send service, so none of this is re-verified when you switch:

  • Your sending domain. The same verified-domain check refuses the same from.
  • Your suppression list. A bounced or complained address is dropped from either call.
  • Your delivery log. The same rows, the same dlv_ ids, the same statuses, in Deliveries beside everything your automations send.
  • Your credit and rate limit. One meter for both.
  • Your templates. The same version, module or doc, rendered by the same renderer as the Send test on the template’s page.

They can run side by side: same organization, same key, same app. The facade resolves the app from the URL and the SDK from the client’s sourceId, and both write deliveries of the same kind against the same source. So a migration is per call site, not a cutover: move the sends you are touching anyway, leave the rest, and nothing in the delivery log tells them apart.