Sign in to fill these examples with your own keys. Sign in Start free trial

Send emails with Inngest

Install the Inngest adapter and call sendEmail inside step.run.

What you'll build

An Inngest function that sends a welcome email when a user signs up. The adapter runs the send as a step, so Inngest retries it and replays never send twice.

Before you start

  • An API key with Send only access. Full access also works. See Create an API key. Support keys cannot send.
  • A verified sending domain. See Add and verify a domain. from must be an address on it.
  • A cluster that is ready to send (sending_ready is true). See Clusters.
  • Your team id and cluster id. Both are on the API keys page.
  • Node.js 18 or later.

Install

Install the adapter with inngest.

install.sh
1npm install @postshiba/inngest inngest

Send your first email

Call sendEmail with the function's step, your config, and the message. Pass idempotencyKey from event.id so a replayed step does not send twice.

send.ts
1import { Inngest } from "inngest" 2import { sendEmail } from "@postshiba/inngest" 3 4const inngest = new Inngest({ id: "app" }) 5 6export const sendWelcome = inngest.createFunction( 7 { id: "send-welcome" }, 8 { event: "app/user.created" }, 9 async ({ event, step }) => { 10 return await sendEmail( 11 step, 12 { 13 apiKey: "YOUR_API_KEY", 14 teamId: "KjkAJW", 15 clusterId: "NmQpXr", 16 }, 17 { 18 from: "hello@mail.example.com", 19 to: [event.data.email], 20 subject: "Welcome", 21 text: "Glad you are here.", 22 idempotencyKey: event.id, 23 }, 24 ) 25 }, 26)

Serve the function with your Inngest handler, then send an app/user.created event. sendEmail runs a step named postshiba-send and returns { queued, messageId }. The message then shows under Events in the dashboard.

clusterId is required. The adapter posts to POST /api/v1/teams/:team_id/clusters/:cluster_id/sends with an Idempotency-Key header.

Attachments and templates

Attach a file

Pass attachments with filename, contentType, and base64 content. Up to 10 files, 18 MB in total. See Attachments for allowed types.

attachment.ts
1await sendEmail(step, config, { 2 from: "hello@mail.example.com", 3 to: [event.data.email], 4 subject: "Your photo", 5 text: "See attached.", 6 attachments: [ 7 { 8 filename: "photo.png", 9 contentType: "image/png", 10 content: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==", 11 }, 12 ], 13 idempotencyKey: event.id, 14})

Send a published template

Pass template with the alias or public id and its variables. Leave out html and text.

template.ts
1await sendEmail(step, config, { 2 from: "hello@mail.example.com", 3 to: [event.data.email], 4 template: { id: "welcome", variables: { name: event.data.name } }, 5 idempotencyKey: event.id, 6})

Keep the key safe

Use a Send only key in your functions. It can send email and call GET /api/v1/users/me. Every other request returns 403 forbidden.

If this key leaks Send only Full access
Send mail from your verified domains Yes Yes
Read events, recipients, templates, and sent messages No Yes
See SMTP passwords and webhook secrets No Yes
Change clusters, domains, credentials, webhooks, and suppressions No Yes
  • Keep Full access keys for setup scripts, the CLI, and MCP. Don't deploy one with your functions.
  • Set POSTSHIBA_API_KEY in the environment of the app that serves your Inngest functions, and pass process.env.POSTSHIBA_API_KEY as apiKey. Don't commit it, log it, or put it in event data.
  • Send from the function. Never put a key, even a Send only key, in browser or mobile code.
  • Create one key per app and environment, so your Inngest production and branch environments never share one. If a key leaks, delete it on API keys and create a new one. Deleting stops it straight away.

Troubleshooting

The adapter maps API errors onto Inngest errors before they leave the step. PostShibaHttpError carries status, code, and retryable.

Status error What the adapter does
401 Invalid token or no user signed in Throws NonRetriableError. Check the API key.
403 forbidden, domain_unverified, or kyc_required Throws NonRetriableError. Support keys cannot send, so use a Send only key and a verified from. While your team is pending production approval, send only to team members, live inbox addresses, and inbox forward-to addresses.
409 idempotency_conflict Throws NonRetriableError. The same idempotencyKey was used with a different message.
422 invalid Throws NonRetriableError. code and the error message say which field.
429 throttled Throws RetryAfterError, and Inngest waits one hour before the next attempt. Do not send the same message from another system in that window. See What happens at your cap.

Every other status is on Errors.

Next steps