{"schema_version":"1.0","publisher":"Primitive SDKs","canonical_url":"https://test.abhinandan.one/node-sdk-sending-email","markdown_url":"https://test.abhinandan.one/node-sdk-sending-email.md","article":{"id":"ecb2a606-4d36-4ac0-a18a-6321f5048446","article_slug":"node-sdk-sending-email","parent_article_slug":null,"parent_article_title":null,"kind":"guide","published_at":"2026-08-11T18:55:05.568764+00:00","keywords":["client.send","client.reply","client.forward","wait mode","SendAttachment","idempotencyKey"],"meta_description":"client.send, client.reply, and client.forward deliver outbound mail from @primitivedotdev/sdk with wait-mode delivery status and attachment control.","og_image_url":null,"source_file_paths":["sdk-node/README.md","sdk-node/src/api/index.ts"],"recording_id":null,"replayable":false,"task_name":"Sending, Replying, and Forwarding Email","category":"Node.js SDK","summary":null,"description":"Use client.send, client.reply, and client.forward to deliver outbound mail from the Node.js SDK, with control over threading, wait-mode delivery status, and inline or referenced attachments.","content_kind":"repo_page","content_markdown":"Use `client.send`, `client.reply`, and `client.forward` to deliver outbound mail from `@primitivedotdev/sdk`. Reach for this page when you already have a `client` from `primitive.client({ apiKey })` and need to send a new message, answer an inbound one, or hand it off to someone else.\n\nAll three methods return as soon as Primitive accepts the message for delivery, unless you opt into [wait mode](email-model) for a delivery status. For the inbound side of the flow (turning a webhook into a [ReceivedEmail](node-sdk-receiving-email)), see [Receiving Inbound Email](node-sdk-receiving-email).\n\n## Send a new email\n\n`client.send` delivers a brand-new message. It requires `from`, `to`, `subject`, and at least one of `bodyText` or `bodyHtml`.\n\n```ts\nimport primitive from \"@primitivedotdev/sdk\";\n\nconst client = primitive.client({\n  apiKey: process.env.PRIMITIVE_API_KEY!,\n});\n\nconst result = await client.send({\n  from: \"Support <support@example.com>\",\n  to: \"alice@example.com\",\n  subject: \"Hello\",\n  bodyText: \"Hi there\",\n  wait: true,\n  waitTimeoutMs: 5000,\n});\n\nconsole.log(result.id, result.status, result.queueId, result.deliveryStatus);\n```\n\n`send` validates inputs before making a request:\n\n- `from` and `to` must be non-empty (`from` up to 998 chars, `to` up to 320 chars) and `to` must parse as a valid address or `Name <addr>` form.\n- `subject` must be non-empty.\n- One of `bodyText` or `bodyHtml` is required.\n- `waitTimeoutMs`, when set, must be an integer between 1000 and 30000.\n\n## Reply to an inbound email\n\n`client.reply` answers a [ReceivedEmail](node-sdk-receiving-email) you already normalized with `primitive.receive(...)`. Recipients, the `Re:` subject, and threading headers (`In-Reply-To`, `References`) are all derived server-side from the inbound row, you never set them.\n\n```ts\nawait client.reply(email, \"Thank you for your email.\");\n```\n\nThe bare-string form is shorthand for `{ text: \"...\" }`. Pass an object for more control:\n\n```ts\nawait client.reply(email, {\n  text: \"Thanks for your email.\",\n  html: \"<p>Thanks for your email.</p>\",\n  attachments: [\n    {\n      filename: \"report.txt\",\n      content_base64: Buffer.from(\"hello\").toString(\"base64\"),\n    },\n  ],\n  wait: true,\n});\n```\n\n<Tip>\n\n`reply()` defaults the From address to the inbound recipient (the address that received the email). Pass `from` explicitly when your verified outbound domain differs from your inbound domain:\n\n```ts\nawait client.reply(email, {\n  text: \"Thanks for your email.\",\n  from: \"notifications@outbound.example.com\",\n});\n```\n\n</Tip>\n\n<Warning>\n\n`reply()` does not accept a `subject` field. Gmail's Conversation View needs both a `References` match and a normalized-subject match to thread correctly, so a custom subject silently breaks threading for a chunk of recipients. Use `client.send(...)` if you need full subject control.\n\n</Warning>\n\nIf the inbound row isn't in a state Primitive can reply to (it was rejected at ingestion, its content was discarded, or it has no recipient recorded), the API returns `inbound_not_repliable` (HTTP 422) and the SDK throws. Check [Node.js SDK Errors](node-sdk-errors) for how to handle this.\n\n## Forward an inbound email\n\n`client.forward` re-sends an inbound message to a new recipient.\n\n```ts\nawait client.forward(email, {\n  to: \"ops@example.com\",\n  bodyText: \"Can you take this one?\",\n});\n```\n\n`forward` has no `wait` option: it always returns as soon as Primitive accepts the message.\n\n## Control delivery confirmation with wait mode\n\nPass `wait: true` on `send` or `reply` to hold the HTTP response open until the first downstream SMTP delivery outcome, or until `waitTimeoutMs` elapses (default 30000 ms, and it must be an integer between 1000 and 30000 when you set it). By default all three methods return the moment Primitive accepts the message, with no signal about what happened downstream. This is [wait mode](email-model); see that page for the full [delivery status](email-model) reference (`delivered`, `bounced`, `deferred`, `wait_timeout`).\n\n<Steps>\n\n<Step title=\"Decide whether you need a delivery outcome\">\n\nUse the default (no `wait`) for fast response times when your caller doesn't need SMTP confirmation. Use `wait: true` only when the caller must know whether the message actually reached the recipient's MTA before you respond.\n\n</Step>\n\n<Step title=\"Set wait and a generous timeout\">\n\n```ts\nconst result = await client.send({\n  from: \"Support <support@example.com>\",\n  to: \"alice@example.com\",\n  subject: \"Hello\",\n  bodyText: \"Hi there\",\n  wait: true,\n  waitTimeoutMs: 5000,\n});\n```\n\n</Step>\n\n<Step title=\"Configure your runtime's own request timeout\">\n\nWhen you use `wait: true`, configure your transport or serverless runtime with a request timeout long enough for SMTP delivery, or your own infrastructure will cut the connection before Primitive responds.\n\n</Step>\n\n<Step title=\"Read result.deliveryStatus\">\n\n```ts\nconsole.log(result.deliveryStatus);\n// \"delivered\" | \"bounced\" | \"deferred\" | \"wait_timeout\"\n```\n\n`wait_timeout` means no outcome was observed in time, treat it as \"outcome unknown,\" not \"failed.\" The send may still complete after the response returns.\n\n</Step>\n\n</Steps>\n\n## Attach files\n\nPass `attachments` with base64 `content_base64` for anything at or below the inline size cap; the SDK's default inline threshold is 25 MiB, the server's inline/offload threshold:\n\n```ts\nawait client.send({\n  from: \"Support <support@example.com>\",\n  to: \"alice@example.com\",\n  subject: \"Report\",\n  bodyText: \"See attached.\",\n  attachments: [\n    {\n      filename: \"report.txt\",\n      content_type: \"text/plain\",\n      content_base64: Buffer.from(\"hello\").toString(\"base64\"),\n    },\n  ],\n});\n```\n\nFor larger files, upload the object with [Primitive Payloads](node-sdk-payloads) first, then deliver it by reference with `payloadAttachments` instead of inlining the bytes. Each entry takes the finalized object's 64-char lowercase-hex Merkle `root` (`PushResult.merkleRoot`), a `filename`, an optional `contentType`, and the hex-encoded `cek` (`PushResult.cek`) the recipient needs to decrypt:\n\n```ts\nawait client.send({\n  from: \"Support <support@example.com>\",\n  to: \"alice@example.com\",\n  subject: \"Archive\",\n  bodyText: \"See the attached archive.\",\n  payloadAttachments: [\n    {\n      root: pushed.merkleRoot,\n      filename: \"large-file.zip\",\n      cek: pushed.cek,\n    },\n  ],\n});\n```\n\n<Tip>\n\nIf you don't want to choose inline vs. reference yourself, use `client.sendAttachment(...)` (the `SendAttachmentInput` shape) with a single `attachment.content` or `attachment.path`. It picks inline or upload-and-reference for you based on size, defaulting the threshold to 25 MiB (`inlineThreshold`).\n\n</Tip>\n\nAt most one `payloadAttachments` entry is supported per send in v1; there's no upper size ceiling on the referenced object beyond what [Primitive Payloads](node-sdk-payloads) supports.\n\n## Thread a new send under an existing conversation\n\n`send` (not `reply`) accepts explicit threading headers when you need to continue a thread outside the reply-to-inbound flow:\n\n```ts\nawait client.send({\n  from: \"Support <support@example.com>\",\n  to: \"alice@example.com\",\n  subject: \"Re: Hello\",\n  bodyText: \"Following up on this.\",\n  thread: {\n    inReplyTo: \"<parent-message-id@example.com>\",\n    references: [\"<root@example.com>\", \"<parent-message-id@example.com>\"],\n  },\n});\n```\n\n`thread.references` accepts at most 100 values, and the joined header must stay under 8 KiB.\n\n## Retry safely with idempotency keys\n\nPass `idempotencyKey` as a per-call [request option](node-sdk-request-options) on `send` to make retries safe. Reusing the same key returns the original response instead of sending a duplicate:\n\n```ts\nawait client.send(\n  { from: \"support@example.com\", to: \"alice@example.com\", subject: \"Hello\", bodyText: \"Hi there\" },\n  { idempotencyKey: \"customer-key-abc123\" },\n);\n```\n\nCheck `result.idempotentReplay` to tell whether a response was replayed from a prior send (`true`) or is fresh (`false`).\n\n## Next steps\n\n<CardGroup cols={2}>\n\n<Card title=\"Receiving Inbound Email\" href=\"node-sdk-receiving-email\">\n\nTurn a raw inbound webhook into the ReceivedEmail object you pass to reply and forward.\n\n</Card>\n\n<Card title=\"Request Options and Idempotency\" href=\"node-sdk-request-options\">\n\nApply per-call timeouts, abort signals, and idempotency keys to any client method.\n\n</Card>\n\n<Card title=\"Primitive Payloads: Streaming Large Attachments\" href=\"node-sdk-payloads\">\n\nUpload and reference multi-gigabyte attachments in bounded memory.\n\n</Card>\n\n<Card title=\"Node.js SDK Errors\" href=\"node-sdk-errors\">\n\nLook up inbound_not_repliable and other PrimitiveApiError codes.\n\n</Card>\n\n</CardGroup>","canonical_base_url":"https://test.abhinandan.one","seo_indexing_enabled":true,"last_modified":"2026-08-21T18:22:43.359885+00:00","video_url":null,"voiceover_url":null,"tools_used":[],"demonstrated_by":[],"steps":[],"related_links":[],"intro":null,"prerequisites":[],"verification":[],"troubleshooting":[],"suggest_edit_url":"https://github.com/abhi-browzer/primitive-sdks/edit/main/sdk-node/README.md","raise_issue_url":"https://github.com/abhi-browzer/primitive-sdks/issues/new?title=Docs+feedback%3A+Sending%2C+Replying%2C+and+Forwarding+Email&body=Page%3A+https%3A%2F%2Ftest.abhinandan.one%2Fnode-sdk-sending-email","page_feedback_enabled":true,"verified_ref":null,"verified_at":"2026-08-11T18:38:45.205849+00:00"}}