Share an Email With an Expiring Link

Send an email via API, then generate a share link that expires on schedule.

Introduction

30 Second Summary

Someone asks what an email actually said, and the answer is almost always a screenshot pasted into a chat thread. Screenshots get cropped, they get forwarded, and they never stop working.

In this project, you will send an email through the Resend API and mint a read only link to that exact message. You will choose how long the link lives, open it with no account, and watch it die on schedule.

What You'll Build

You will paste one link into a chat, watch someone with no account read your email in their browser, and then see that same link refuse to load once the window closes.

By the end of this project, you'll have:

  • A share link to a real email you sent, which anyone can open in a private browser window without signing in to anything.
  • Your own record of what a visitor sees once the link has expired, because you will wait out the window and reload it yourself.
  • One script you can run that sends an email and prints a ready to paste share link, the piece you would drop straight into a real support tool.
  • Secret Mission: Extend your script so the expiry window is something you pass in, then mint a fresh link once the old one has died.

Are there any prerequisites?

You need a Resend account and an API key, and you create both in Step 1. Sending also needs a sender address your account is allowed to use, so that setup is part of the first step too.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters. Naming the goal now makes every step that follows much easier to follow.

Create Your Resend Account and API Key

Sending an email from a script means proving to an email provider that the request really came from you. Resend handles that with an API key.

A key you have not saved anywhere is a key your script cannot use. So this step is about creating an account, minting a key, and parking it somewhere your code can read it later.

In this step, get ready to:
  • Create a Resend account and sign in to the dashboard.
  • Generate an API key called send-and-share-key.
  • Save the key in a .env file inside your project folder.
Create your Resend account
  • Open the Resend sign up page in your browser.
  • Enter your email address in the Email field.
  • Enter a password in the Password field.
  • Click Create account.
  • Open the confirmation email from Resend and follow its link if you are asked to verify your address.

You land on the Resend dashboard once your account is confirmed.

Prefer a social login

The sign up page also offers Log in with Google and Log in with GitHub. Either one skips the email and password fields entirely.

You do not need your own domain yet

Resend gives you a test sender address, onboarding@resend.dev, which covers everything you do in this project.

Skip any prompt to add and verify a domain for now. That address is for testing only, so a real product would still need a verified domain.

Generate your API key

Your script never types your password. It sends an API key on every request, and Resend uses that key to work out which account the call belongs to.

You only see the key value once

Resend shows the full key at the moment you create it. You cannot view or edit an API key value after it has been created, so copy it before you close the dialog.

  • In the dashboard's left sidebar, click API keys.
  • Click the create button on the API keys page.
  • Enter send-and-share-key in the Name field.
  • Leave Permission set to full access, so this one key can send emails and create share links later.
  • Submit the form to create your key.
  • Copy the key value, which starts with re_, to your clipboard.
  • Close the dialog once the key is on your clipboard.

Your new key now sits in the API keys list with its value masked.

Closed the dialog before copying?

  • Create a second key with the same name. There is no way to reveal the value of a key that already exists.
  • Tidy up the unused key from the API keys page with the More options button, then Remove API key.

Why can't I see my Resend API key value anymore?

Save your key in a .env file

A key pasted straight into your code is a key you will push to a public repo one day. An environment file keeps the value next to your project without ever putting it in your script.

  • Return to the terminal you already have open in your project folder, then create your environment file with the command for your system:

macOS

touch .env
  • Confirm the file exists by running this command:
ls -a

You should see .env listed in the output.

Windows

New-Item .env -ItemType File

PowerShell prints a directory listing with .env in the Name column, which confirms the file exists.

  • Open .env in your code editor.
  • Add this line, swapping your-api-key-here for the key you copied:
RESEND_API_KEY=your-api-key-here

What does this line do?

  • RESEND_API_KEY is the variable name the Resend SDK expects, so your script can read the key with process.env.RESEND_API_KEY.
  • Keeping the value in .env means the key stays out of the code you share.
  • Save .env.

Before you run the next command, what do you think it prints if your key line saved correctly?

  • Check that your key is readable by running this command in your terminal:
node -e "console.log(require('fs').readFileSync('.env','utf8').includes('RESEND_API_KEY=re_') ? 'Key saved' : 'Key missing')"

You should see Key saved in the terminal. Your key is on disk and your script can reach it without the value ever appearing in your code.

Seeing Key missing?

  • Check the line starts with RESEND_API_KEY= and has no spaces around the equals sign.
  • Check the value you pasted starts with re_. A half copied key will not match.
  • Check that .env sits in the same folder your terminal is in.

Why isn't my .env file being read?

Your account is live and your key is saved. Next up, you'll use that key to send a real email through the API and catch the email id that comes back in the response.

Send a Test Email and Capture Its Id

Your API key is sitting in a .env file doing nothing. A key on its own proves nothing about whether you can actually send mail.

The share link you build later hangs off a real message. Resend can only share an email that already exists, and it identifies that email by an id it hands back when you send.

So this step is about getting that id. You'll write a small script, send one test email, and read the id out of the response.

In this step, get ready to:
  • Write a script that sends an email through the Resend API.
  • Run the script and read the JSON response in your terminal.
  • Copy the email id out of that response for the next step.
Create your send script

Everything in this project runs from one file. You'll start it with the send request and grow it into the full send-and-share script by the end.

  • In the terminal you already have open in your project folder, create the script file by running this command:
touch send-and-share.js
  • Confirm the file landed in the right place by running:
ls
  • You should see send-and-share.js and .env listed in the output.

Only seeing send-and-share.js?

Files starting with a dot are hidden by default, so run ls -a to see .env too.

If .env genuinely isn't there, you're in a different folder from the one where you saved your key. Where did my .env file go?

Send your first email

The send request needs four things: who it's from, who it's to, a subject, and a body.

Which from address can I use?

Resend only lets you send from an address it knows belongs to you. Verifying your own domain takes DNS changes and time you don't need to spend here.

Resend gives every account the shared testing sender onboarding@resend.dev, which works with no setup. Mail from it can only go to the email address you signed up with.

  • Open send-and-share.js in your editor.
  • Build the send request by pasting this code into the empty file:
// Read the API key you saved earlier out of the environment
const apiKey = process.env.RESEND_API_KEY;

// Ask Resend to send one email on your behalf
const sendResponse = await fetch("https://api.resend.com/emails", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${apiKey}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    from: "onboarding@resend.dev",
    to: "[[YOUR_EMAIL=\"the email address you signed up to Resend with\"]]",
    subject: "Test email from my share script",
    html: "<p>This is the message I am going to share with a link.</p>",
  }),
});

// Print whatever Resend sends back so you can read the id
const sendResult = await sendResponse.json();
console.log(sendResult);

What does this code do?

The Authorization header carries your API key. It is how Resend knows the request belongs to your account.

The body holds the four fields that describe the message: from, to, subject and html.

That last console.log line is the one that matters most here. It puts Resend's answer on your screen, and the id you need is inside it.

  • Save send-and-share.js.

Before you run it, think about one thing. Your key lives in .env, and the script reads it from the environment. Do you think those two are connected yet?

  • Find out by running your script:
node send-and-share.js

You get an error about a missing or invalid API key, not a sent email. Nothing loaded .env for you, so process.env.RESEND_API_KEY came back empty.

Why the file didn't load itself

A .env file is just text sitting on disk. Something has to read it and push those values into the environment before your code can see them.

Node has this built in with the --env-file flag, so you don't need to install a package for it.

  • Hand your key to the script by running it with the env file flag:
node --env-file=.env send-and-share.js

This time you get a JSON object printed in your terminal, and it contains an id field.

Still seeing an error instead of an id?

If the message mentions the to address, check you replaced the placeholder with the exact address you signed up to Resend with. The shared testing sender refuses anything else.

If it still mentions the API key, open .env and confirm the line reads RESEND_API_KEY= followed by your key, with no quotes or spaces around it.

Stuck on something else? Why is my Resend send request failing?

Capture the email id

That id is the handle for this exact message. The share endpoint in the next step takes it in the URL, so it needs to survive past this terminal session.

  • Find the id field in the printed response.
  • Record the id here: your email id

Now for the proof that the send actually worked. Before you look, what do you expect to find in the inbox of the address you sent to?

  • Open the inbox of the address you put in the to field.

There's an email with the subject Test email from my share script. The id you just recorded points at that message.

You have a real sent message and the id that names it. Next up, you'll trade that id for a share link that anyone can open.

Generate an Expiring Share Link

You sent a real email through the API and pulled its id out of the response. That id is a handle on a message that actually exists in Resend.

Right now that email is only visible to someone with dashboard access. The share endpoint turns your email id into a URL anyone can open, and it stops working on a schedule you choose.

In this step, get ready to:
  • Call the share endpoint on the email id you copied.
  • Set expires_in to a short window so the link dies quickly.
  • Save the share URL that comes back in the response.
Call the share endpoint with your email id

The share endpoint takes the id of an email you already sent and hands back a public URL for it. You control how long that URL stays alive with expires_in.

  • Switch back to the terminal you used to run send-and-share.js.
  • In the .env file from earlier, copy the value of RESEND_API_KEY.

Before you press enter, what do you expect back: the contents of the email, or a link to it?

  • Ask for a share link by running this command, with your own email id and API key pasted in:
curl -X POST 'https://api.resend.com/emails/[[EMAIL_ID="your email id"]]/share' \
     -H 'Authorization: Bearer your-api-key-here' \
     -H 'Content-Type: application/json' \
     -d '{"expires_in": "10m"}'

What does this command do?

  • The /emails/{email_id}/share path tells Resend which single email you want a link for.
  • The Authorization header carries your API key, which is how Resend knows the request is yours.
  • The value 10m gives the link ten minutes of life. Leave it out and the link lasts 48 hours, which is also the maximum.
  • Read the response printed in your terminal.

You get back a small JSON object with three fields. The url field holds a link that starts with https://resend.com/shared?token=.

No share link in the response?

  • If the response complains about the duration, check that expires_in reads 10m. Invalid or over-limit durations are rejected.
  • If the response complains about the email, check you pasted only the id value from your send response, not the whole object.
  • If the response complains about authentication, check you replaced your-api-key-here with the key from your .env file.

Why is my Resend share request failing?

Save the share url before it scrolls away

The url value is the thing you would hand to a customer or a teammate. You open it in the next step, so it needs to survive past this terminal session.

  • Copy the whole url value from the response, including the token after ?token=.
  • Paste it here so you have it in the next step: your share URL
  • Compare the id in this share response with the email id you pasted into the command.

The two ids match, which proves your link points at the exact email you sent and not some other message in the account.

Start the clock now

Your ten minutes started the moment the response came back. Open the link soon, because a later step asks you to watch it expire.

You have a live link to a real email sitting in your clipboard. Next up, you'll open it in a private browser window and find out whether it really works with no account at all.

Open the Share Link with No Sign-In

You finished the last step holding a share URL. Right now it's just a string of characters, and nothing has proven it works for someone who isn't you.

That's the whole promise of the Resend share endpoint. Anyone holding the link can read the email with no account and no sign-in.

Your normal browser window is already signed into the Resend dashboard, so it can't tell you anything. A private window gives you a clean visitor with no cookies and no session.

In this step, get ready to:
  • Open a private browser window with no Resend session in it.
  • Paste in your share URL and read the email as an anonymous visitor.
  • Confirm the subject and body match the test email you sent.
Open a private browser window
  • Switch to the browser window where you're signed into the Resend dashboard.
  • Open a private window by pressing Cmd+Shift+N (macOS) or Ctrl+Shift+N (Windows) if you use Chrome, Edge, or Safari.
  • Open a private window by pressing Cmd+Shift+P (macOS) or Ctrl+Shift+P (Windows) instead if you use Firefox.

A new window opens with a message telling you that you're browsing privately. None of your Resend login carries over into it.

Why a private window?

A private window starts with no cookies and no session, so the page loads exactly the way a stranger would receive it.

Open the link in your normal window and you'd never know what granted access. Your dashboard login could be doing the work instead of the link.

Open the share link with no account

Before you paste the link, make a call. Does this page show the email straight away, or does it stop you at a sign-in screen first?

  • Copy the share URL you noted down at the end of the last step.
  • Paste it into the address bar of your private window.
  • Press Enter to load the page.

The email opens straight away. You get the subject and body of your test email, with no sign-in screen anywhere in the way.

Not seeing your email?

Share URLs are long, so check you copied the whole thing. A link that lost its last few characters won't resolve.

You set a short expiry window when you called the share endpoint, so it may already have lapsed. Call the share endpoint again on the same email id to mint a fresh link.

Still stuck? Why isn't my share link opening the email?

Check what an anonymous visitor sees
  • Read the subject line on the page and check it matches the subject your script sent.
  • Read the body on the page and check it matches the body your script sent.
  • Scroll the page and look for a sign-in prompt, an account menu, or a dashboard link.

There's nothing on the page to sign into. You're looking at a read only view of that one message, and the URL itself is the entire credential.

What the link actually grants

Anyone holding this URL can read the message. Treat it the way you'd treat a password for that one email.

The expiry window is what keeps that exposure small. Resend defaults the window to 48 hours, and that's also the longest you can ask for.

Your link works for a total stranger. Next up, you'll let the clock run out and see exactly what that stranger gets once the window closes.

Watch the Share Link Expire

Last step you opened the share URL in a private window and read the whole email with no account and no sign-in. That is half of what a share link promises.

The other half is the link going dead on schedule. Until you watch that happen, the expires_in value you sent Resend is a number you are taking on faith.

So this step is mostly waiting. You will mark the moment your window closes, reload the exact same URL, and record what a stranger sees on the other side of it.

In this step, get ready to:
  • Work out the exact clock time your share link stops working.
  • Reload the same share URL once that window has passed.
  • Record what an anonymous visitor sees once the link has expired.
Work out when your share link goes dark

The countdown started when the share link was created, not when someone opened it. Your window has been running since that share response came back.

  • Return to your terminal in your project folder.
  • Check your machine's current clock time by running this command:
date

The output prints today's date and your current time down to the second.

  • Add the expiry window you set in the previous step to that clock time.
  • Note the moment your link stops working: the time your link stops working.

Why the clock is already running

The window is measured from the moment the share link is created. Every minute you spent reading the email in the private window came out of it.

Resend caps that window at 48 hours, and uses 48 hours when you leave expires_in out of the request. There is no such thing as a share link that lives forever.

Reload the link after your window closes
  • Wait until your clock passes the expiry time you noted.

Before you reload, make a call. Does the page still render the email, does it hand you an error, or does it ask you to sign in?

  • Switch back to the private browser window still sitting on your share URL.
  • Reload the page.

The subject and body you read a few minutes ago are gone. The same URL now answers with an error instead of the message.

Still seeing the email?

Your browser may be serving the copy it already has. Force a fresh request with Cmd+Shift+R (macOS) or Ctrl+Shift+R (Windows).

Run the date command again and compare it to the expiry time you noted. A window that has not fully passed still serves the email.

My Resend share link still loads the email after its expiry window passed

  • Note the wording the page gives you, ready for the checkpoint below.

What just happened to your link?

Nothing about the URL changed. The expiry is enforced on Resend's side, so the same address that worked five minutes ago is simply refused now.

The email itself is untouched and still sits in your account. Only the anonymous view of it closed.

You have now seen both ends of a share link's life. Next up, you'll fold the send call and the share call into one script that hands you a ready to paste link in a single run.

Combine the Calls Into One Script

You've now proved the whole flow by hand. You sent an email, read its id out of the JSON response, pasted that id into a share call, and watched the link work and then expire.

Copying an id between two terminal commands works once. A support tool can't do that for you, so this step wires both calls into one script that prints a link you can paste straight into a ticket.

In this step, get ready to:
  • Store the email id from the send response in a variable.
  • Add the share call to the same script.
  • Run the script end to end and get a ready-to-paste link.
Store the email id in a variable

Your script prints the whole send response and you read the id off the screen. The share call needs that id, so store it somewhere the rest of the script can reach.

  • Switch back to send-and-share.js in your editor.
  • Find the line at the bottom that logs the send response to the console. It looks like this:
const data = await response.json();
console.log(data);
  • Replace the console.log line so the id lands in its own variable, like this:
const data = await response.json();

// Keep the id so the share call can use it later
const emailId = data.id;
console.log("Email sent, id:", emailId);
  • Save send-and-share.js.
  • Send a fresh test email by running your script:
node send-and-share.js
  • Check your terminal for a line reading Email sent, id: followed by a long id.

Seeing undefined instead of an id?

If your script parses the send response into a name other than data, use that name on the const emailId line.

The send call has to succeed before there is an id to read. If the response holds an error message instead, fix the send before moving on.

Why is my email id coming back undefined?

Add the share call to the same script

The share endpoint takes the id of an email you already sent and hands back a read-only link. Your script holds that id now, so it can make the call itself.

  • Mint the share link from inside your script by copying this code to the bottom of send-and-share.js, below the line that prints the email id:
// Ask Resend for a read-only link to the email we just sent
const shareResponse = await fetch(`https://api.resend.com/emails/${emailId}/share`, {
  method: "POST",
  headers: {
    // The same API key that authenticated the send call
    Authorization: `Bearer ${process.env.RESEND_API_KEY}`,
    "Content-Type": "application/json",
  },
  // A human-readable duration, so this link dies 10 minutes after Resend creates it
  body: JSON.stringify({ expires_in: "10m" }),
});

// The share response carries the finished link in its url field
const shareData = await shareResponse.json();
console.log("Share this link:", shareData.url);

What does this code do?

  • The id sits in the URL path, so emailId decides which email gets shared.
  • The same RESEND_API_KEY authenticates both calls, so there is nothing new to set up.
  • expires_in takes a human-readable duration. 10m gives you a link that stops working ten minutes after Resend creates it.
  • shareData.url is the full share URL, ready to hand to someone with no Resend account.

Why 10 minutes and not a week?

Share links always expire. If you leave the duration out, Resend defaults to 48 hours, which is also the maximum it will accept.

A short window keeps this run quick to test. Anything over the limit comes back as a validation error rather than a link.

  • Save send-and-share.js.
Run the script end to end

One run should now do everything you did by hand across the last four steps.

Before you run it, how many lines do you expect the script to print, and which one carries the link?

  • Send and share in one go by running this command:
node send-and-share.js

You'll see the Email sent, id: line first, then a Share this link: line holding a https://resend.com/shared?token= URL.

  • Copy the share URL from your terminal.
  • Open a new private browser window.
  • Paste the URL into the address bar, then press Enter.
  • Confirm the subject and body of the fresh test email render with no account signed in.

No link in the output?

If the terminal prints undefined where the link should be, the share call returned something other than a link. Add console.log(shareData); to see the whole response.

Ten minutes is a short window. If the page refuses a link you copied a while ago, run the script again for a fresh one.

Why is my Resend share call not returning a URL?

✔️ Awesome, I've got everything!

Great, double check you've saved send-and-share.js and move on.

ⓧ I'd like to double check the full code

Scroll up through your file and confirm all three pieces are present, in this order:

  • The send call you wrote earlier, ending with const data = await response.json();.
  • The const emailId = data.id; line and the log that prints it.
  • The share fetch block and the log that prints shareData.url.

Secret mission

Set the Expiry Window from the Command Line

Your script hands out links on one fixed schedule. Add a command line argument so you can mint a 10 minute link for a quick check, or a 1 day link for someone in another timezone, without editing the code every time.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. Everything you built sits inside Resend's free plan, so nothing here is adding to a bill.

Resources you used:

  • The Resend API key you generated for this project.
  • The share links printed by send-and-share.js.
  • Your local project folder holding send-and-share.js and your .env file.

Your API key is still live

The key sitting in your .env file can still send email from your account until you delete it. Anyone who gets hold of that file can send as you.

Keep everything running

No action needed. Choose this if you're still actively building on top of the send and share flow.

  • Leave your Resend API key in place so send-and-share.js keeps working.
  • Keep send-and-share.js and your .env file where they are.

Your Resend account stays open whichever option you pick. The free plan covers 3,000 emails a month, so an idle account costs nothing.

Pause - I'll come back to this later

Nothing runs in the background here, so pausing means taking your API key out of play. Your files stay exactly where they are.

  • Return to the Resend dashboard in your browser.
  • Go to the API Keys page.
  • Find the row for the key you created for this project.
  • Delete that key.
  • Delete the RESEND_API_KEY line from your .env file.

When you come back, generate a fresh key and paste it into .env. The rest of your script works unchanged.

Delete - I don't want to use this again

Remove all project resources and start fresh. Choose this if you're done with the send and share flow.

  • Return to the Resend dashboard in your browser.
  • Go to the API Keys page.
  • Find the row for the key you created for this project.
  • Delete that key.

What happens to the share links?

Every share link you created stops working on its own once the window you set with expires_in passes. There is nothing to delete.

48 hours is the longest any share link can last, so even a forgotten link closes itself within two days.

  • Delete your project folder on macOS or Linux by running this command:
rm -rf [[PROJECT_FOLDER="path to your project folder"]]
  • Delete your project folder on Windows by running this command:
rmdir /s /q [[PROJECT_FOLDER="path to your project folder"]]
  • Confirm the folder is gone by running ls in the folder that used to contain it.

Folder still there?

Check that the path you typed matches the folder holding send-and-share.js. A trailing space or a missing quote around a folder name with spaces will make the command miss.

Still stuck? Why won't my project folder delete?

Nice Work!

Nice Work!

Nice work. You built send-and-share.js, a script that sends an email and prints back a link to that exact message which anyone can open and which stops working on schedule.

You've learned how to:

  • Send an email through the Resend API and pull the email id straight out of the JSON response.
  • Mint a read only share link for that exact email by calling the share endpoint with expires_in set to a short window.
  • Prove the link is both public and temporary by opening it in a private browser window with no account, then reloading it once the window has passed.
  • Secret Mission: An optional challenge that takes your send and share script further.

Ready to quiz yourself?