{"schema_version":"1.0","publisher":"Primitive SDKs","canonical_url":"https://test.abhinandan.one/node-sdk-download-tokens","markdown_url":"https://test.abhinandan.one/node-sdk-download-tokens.md","article":{"id":"1fe4f9dc-61b2-444c-b7f1-0fe96c5e00eb","article_slug":"node-sdk-download-tokens","parent_article_slug":null,"parent_article_title":null,"kind":"guide","published_at":"2026-08-11T18:54:54.181968+00:00","keywords":["generateDownloadToken","verifyDownloadToken","VerifyDownloadTokenResult","signed download token","download-tokens","@primitivedotdev/sdk/webhook"],"meta_description":"Issue and verify short-lived HMAC download tokens that scope access to a single email's raw bytes or attachment bundle in the Node.js SDK.","og_image_url":null,"source_file_paths":[],"recording_id":null,"replayable":false,"task_name":"Signed Download Tokens","category":"Node.js SDK","summary":null,"description":"Generate short-lived, HMAC-signed tokens that scope a download URL to one email's raw MIME bytes or attachment bundle, and verify them before serving the content.","content_kind":"repo_page","content_markdown":"Signed download tokens let you mint a short-lived, HMAC-signed token that scopes access to one email's raw bytes or attachment bundle, so you can hand out a fetchable link without exposing your webhook secret. Use them when a browser or downstream service needs to retrieve that content later from a route you serve.\n\nBoth helpers are exported from the `webhook` subpath of `@primitivedotdev/sdk`, the SDK entry point that also carries signature verification:\n\n```typescript\nimport {\n  generateDownloadToken,\n  verifyDownloadToken,\n} from \"@primitivedotdev/sdk/webhook\";\n```\n\n<Note>\n\n`generateDownloadToken` takes a `GenerateDownloadTokenOptions` object and `verifyDownloadToken` takes `VerifyDownloadTokenOptions` and returns a `VerifyDownloadTokenResult`. All three types are exported alongside the functions, so your editor shows the exact required fields; the SDK is the authoritative reference for them.\n\n```typescript\nimport type {\n  GenerateDownloadTokenOptions,\n  VerifyDownloadTokenOptions,\n  VerifyDownloadTokenResult,\n} from \"@primitivedotdev/sdk/webhook\";\n```\n\n</Note>\n\n<Tip>\n\nChecking whether an inbound email's raw content arrived inline or must be fetched from Primitive's own download URL is a different job. See [Parsing Raw Email (.eml)](node-sdk-parsing-email) for `isRawIncluded` and `decodeRawEmail`. Download tokens are for URLs you mint and serve yourself.\n\n</Tip>\n\n## Mint and verify a token\n\nGenerate the token in the code that already holds the email and the secret, then verify it in the route that serves the bytes.\n\n<Steps>\n\n<Step title=\"Generate the token where you have the email and the secret\">\n\n```typescript\n// server-side: any module that already has the inbound email in hand\nimport { generateDownloadToken } from \"@primitivedotdev/sdk/webhook\";\n\nconst token = generateDownloadToken({\n  emailId: email.id,\n  secret: process.env.PRIMITIVE_WEBHOOK_SECRET!,\n});\n```\n\nFill in the remaining fields from `GenerateDownloadTokenOptions`; your editor lists them and marks which are required.\n\n</Step>\n\n<Step title=\"Embed the token in a URL your own route serves\">\n\n```typescript\nconst downloadUrl = `https://yourapp.example.com/downloads/${email.id}?token=${token}`;\n```\n\nHand this URL to whatever needs the content later. The token, not the path, is what gates access.\n\n</Step>\n\n<Step title=\"Verify before serving any bytes\">\n\n```typescript\n// app/downloads/[emailId]/route.ts (Next.js route handler)\nimport { verifyDownloadToken } from \"@primitivedotdev/sdk/webhook\";\n\nexport const runtime = \"nodejs\";\n\nexport async function GET(\n  req: Request,\n  { params }: { params: { emailId: string } },\n) {\n  const token = new URL(req.url).searchParams.get(\"token\") ?? \"\";\n\n  const result = verifyDownloadToken({\n    token,\n    emailId: params.emailId,\n    secret: process.env.PRIMITIVE_WEBHOOK_SECRET!,\n  });\n\n  // Inspect `result` (a VerifyDownloadTokenResult) and reject before you\n  // read anything off disk or call Primitive's download endpoint.\n  // ... stream the raw bytes or attachment bundle here ...\n}\n```\n\n</Step>\n\n</Steps>\n\n<Warning>\n\nVerify on every request. Do not cache a \"verified once\" flag for a token past its lifetime, and do not skip the email-id check to serve a multi-email download page. A token is scoped to a single email; mint one per email.\n\n</Warning>\n\n<Tip>\n\nBoth functions take the secret directly, so they work outside a `receive()` call, for example in a standalone download route that never sees the original inbound webhook request. Use the same `PRIMITIVE_WEBHOOK_SECRET` you use for [webhook signature verification](node-sdk-webhook-signing).\n\n</Tip>\n\n## Next steps\n\n<CardGroup cols={2}>\n\n<Card title=\"Parsing Raw Email (.eml)\" href=\"node-sdk-parsing-email\">\n\nDecode raw MIME bytes and extract attachments once you've verified access.\n\n</Card>\n\n<Card title=\"Webhook Signature Verification\" href=\"node-sdk-webhook-signing\">\n\nVerify inbound webhook deliveries with the same PRIMITIVE_WEBHOOK_SECRET.\n\n</Card>\n\n<Card title=\"Bundling Attachments\" href=\"node-sdk-attachment-bundling\">\n\nBuild the content-addressed attachment bundle a token can scope access to.\n\n</Card>\n\n<Card title=\"Receiving Inbound Email\" href=\"node-sdk-receiving-email\">\n\nNormalize the inbound webhook into a ReceivedEmail before minting a token for it.\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":null,"raise_issue_url":"https://github.com/abhi-browzer/primitive-sdks/issues/new?title=Docs+feedback%3A+Signed+Download+Tokens&body=Page%3A+https%3A%2F%2Ftest.abhinandan.one%2Fnode-sdk-download-tokens","page_feedback_enabled":true,"verified_ref":null,"verified_at":"2026-08-11T18:38:45.205849+00:00"}}