Chart Your Email Open and Bounce Rates

Send test emails with Resend and chart their open and bounce rates over time.

Introduction

30 Second Summary

You hit send on a batch of emails and then hear nothing back. Did they land, did they bounce, did anyone actually open them?

In this project, you will send real test emails from your laptop and pull the numbers back from Resend with a single API call. Then you will turn those numbers into a chart and a bounce rate warning you can keep running.

What You'll Build

A page you open on your own laptop that draws your open rate and bounce rate day by day, and prints a warning the moment your bounces climb past the limit you set.

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

  • Real test emails landing in your own inbox, each one showing a delivered status in your Resend Emails log, with opened and clicked events appearing after you interact with them.
  • One call to the Email Metrics API that hands you back JSON with your open rate, bounce rate and raw counts, split into one row per day.
  • A live chart in a plain HTML page that plots those daily rates, plus a bounce rate warning that fires when your numbers cross the limit you picked.
  • Secret Mission: an optional challenge to push what you have built further.

Are there any prerequisites?

You need a free Resend account and any code editor, and that is it. No card is required, and the free tier covers the handful of test emails you send here.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters. Getting this clear now makes every step ahead feel like progress instead of guesswork.

Create Your Resend Account and API Key

Every number you chart in this project starts with an email you actually sent. Before you can send anything, Resend needs a way to know the request came from you.

That proof is an API key. In this step you'll create a free account and generate the key that every later request in this project carries.

In this step, get ready to:
  • Create a free Resend account and land on your dashboard.
  • Generate an API key that can both send emails and read your metrics.
  • Save the key value before Resend hides it for good.
Create your free Resend account

Your account is the thing that owns the emails you send and the numbers they produce.

What is Resend?

Resend is an email API. Your code sends a request, Resend delivers the message, and it records what happened to it.

That record is what this whole project runs on. Delivered, bounced, opened and clicked events all get counted for you.

  • Open the Resend website in your browser.
  • Sign up for a free account using your email address or your GitHub account.
  • Open the verification email that Resend sends to your inbox.
  • Click the confirmation link inside that email.

Once your account is verified, Resend takes you straight to your dashboard.

No verification email?

  • Check your spam folder. Mail from a sender you have never received from before often lands there.
  • Give it a minute before you request another one. A second request can invalidate the first link.

My Resend verification email never arrived, what should I check?

Generate an API key with full access

Your laptop has no other way to prove it is you when it calls Resend. An API key is the secret token every request carries.

Your key appears only once

For security reasons, you can only view the API Key once. Copy it the moment it appears, because the dashboard will never show the value again.

  • Click API Keys in the sidebar of your Resend dashboard.
  • Click Create API Key.
  • Enter metrics-project as the name for your key.
  • Select Full access as the permission.
  • Submit the form to create your key.

A long string starting with re_ appears on screen. That is your key value, and this is the only time you will see it.

Why Full access and not Sending access?

Resend offers two permissions. Sending access allows the API key to only send emails, while Full access allows the API key to create, delete, get, and update any resource.

This project does both. You'll send test emails first, then read your metrics back, so a sending-only key would be turned away on the second half.

  • Copy the key value shown on screen.
  • Press Cmd+N (macOS) or Ctrl+N (Windows) in your code editor to open a new untitled tab.
  • Paste the key into that tab and leave the tab unsaved, so the key never lands in a file you might commit later.

Closed the screen before copying?

There is no way to recover the value, so create a replacement instead.

  • Click the More options button next to the key, then Remove API key.
  • Create a fresh key with the same name and permission, and copy it this time.
Confirm your key is listed on the dashboard

The value is safe in your editor now. The dashboard is your proof that the key actually exists on the account.

Before you look, what do you expect to see next to your key's name in the list: the full value again, or something else?

  • Return to the API Keys page in your Resend dashboard.
  • Refresh the page.

Your key is listed by name, along with its permission and the last time it was used. The value itself is nowhere on the page.

Nothing in the list?

  • Refresh once more. A key created seconds ago can take a moment to show up.
  • Check you are signed into the same Resend account you just verified.

My new Resend API key isn't showing in the API Keys list, what should I check?

Your account is live and your key is ready to use. Next up, you'll spend that key on real test emails and watch each one land in your Emails log with a delivered status.

Send Test Emails via the API

Your Resend API key is saved and ready, but nothing has used it yet.

Metrics only exist for mail you have actually sent. An empty account returns an empty chart.

In this step you'll send a few test emails to your own inbox, so there's something real to measure later on.

In this step, get ready to:
  • Send a test email to your own inbox with a single API call.
  • Confirm the email arrives in your inbox.
  • Watch each send appear in the Resend Emails log with a Delivered status.
Send your first test email

Resend's send endpoint takes one POST request with your key in the header and the message in the body.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal on macOS, or PowerShell on Windows, then press Enter.

macOS

  • Replace your-api-key-here with the API key you saved in the last step.
  • Send your first test email by running this command:
curl -X POST 'https://api.resend.com/emails' \
  -H 'Authorization: Bearer your-api-key-here' \
  -H 'Content-Type: application/json' \
  -d '{
    "from": "onboarding@resend.dev",
    "to": ["[[YOUR_EMAIL="your email address"]]"],
    "subject": "Metrics test 1",
    "html": "<p>Test email one. <a href=\"https://www.nextwork.org\">Click this link</a></p>"
  }'

Windows

  • Replace your-api-key-here with the API key you saved in the last step.
  • Send your first test email by running this command:
curl.exe -X POST 'https://api.resend.com/emails' `
  -H 'Authorization: Bearer your-api-key-here' `
  -H 'Content-Type: application/json' `
  -d '{
    "from": "onboarding@resend.dev",
    "to": ["[[YOUR_EMAIL="your email address"]]"],
    "subject": "Metrics test 1",
    "html": "<p>Test email one. <a href=\"https://www.nextwork.org\">Click this link</a></p>"
  }'

What does this command do?

  • -X POST tells the Resend API to create something new, in this case an email.
  • The Authorization header carries your API key as a Bearer token, which is how Resend knows the request is yours.
  • from uses Resend's shared onboarding sender, so you can send before you own a verified domain.
  • to is your own inbox, the only address that shared sender is allowed to mail.

You should see a short JSON response containing an id field. That id is Resend's handle for the message you just sent.

No id in the response?

  • If the response complains about your API key, copy it again from the API Keys page in the Resend dashboard.
  • Check there is no stray space before or after the key inside the quotes.
  • If the error mentions the recipient or a domain, check that the to address is the same inbox you signed up to Resend with.

Why is my Resend send request failing?

Check that the email landed

An id in the response means Resend accepted the message. Delivery is a separate event, and the Emails log is where it shows up.

  • Open your own inbox and look for the email with the subject Metrics test 1.
  • Switch back to the Resend dashboard in your browser.
  • Click Emails in the left sidebar.
  • Find the entry for Metrics test 1 and read its status.

The status reads Delivered once the receiving mail server has accepted your message.

Why the log matters more than your inbox

Your inbox tells you one message arrived. The Emails log records an event for every message, including the ones that bounce or fail.

The metrics endpoint you'll call later counts those same events. What you see in this log is the raw material behind your chart.

Send two more test emails

One message makes a chart with a single point. A few sends give the metrics call something to count.

  • In your terminal, press the up arrow to bring back your send command.
  • Find this line in the command:
    "subject": "Metrics test 1",
  • Change the subject so the line reads like this:
    "subject": "Metrics test 2",
  • Press Enter to send the second email.
  • Press the up arrow again to bring the command back.
  • Change the subject to Metrics test 3.
  • Press Enter to send the third email.

Before you go back to the dashboard, how many entries do you expect in the Emails log, and what status will each one show?

  • Switch back to the Resend dashboard in your browser.
  • Refresh the Emails page.

You should see three entries, Metrics test 1, Metrics test 2 and Metrics test 3, each showing Delivered.

Only seeing one or two entries?

  • Check your terminal for a send that returned an error instead of an id, and run that one again.
  • Give the log a few seconds and refresh again, since a send can take a moment to register.

Why are some of my sends missing from the Resend Emails log?

Your Emails log now holds three delivered messages with real timestamps. Next up, you'll open those emails and click the link inside so the log starts recording engagement too.

Generate Open and Click Events

Your test emails all landed with a Delivered status in the Resend Emails log. That only tells you the message reached the inbox.

Open rate and click rate are the numbers you actually want to chart later. Neither one exists until a real person opens a message and follows a link inside it.

So in this step you become that person. You'll send yourself an email with a link in it, then open it and click through, and watch the events appear against the message.

In this step, get ready to:
  • Send a test email that has a clickable link inside it.
  • Open that email in your own inbox and click the link.
  • Find the opened and clicked events attached to the message in your Emails log.
Send a test email with a link inside it

A click event only exists if there is something to click. Resend rewrites the links inside your HTML body so it can record when one gets followed.

  • Switch back to the terminal you used to send your test emails.
  • Send one more test email, this time carrying a link, by running this command:
curl -X POST 'https://api.resend.com/emails' \
  -H 'Authorization: Bearer your-api-key-here' \
  -H 'Content-Type: application/json' \
  -d '{
    "from": "[[SENDER_EMAIL="the sender address from your last step"]]",
    "to": "[[YOUR_EMAIL="your email address"]]",
    "subject": "Metrics click test",
    "html": "<p>Testing open and click events. <a href=\"https://nextwork.org\">Visit NextWork</a></p>"
  }'

What does this command do?

  • The Authorization header carries your API key, so Resend knows the request belongs to your account.
  • The html field is the part that matters here. It holds the anchor tag Resend needs in order to record a click.
  • Sending to your own address means you can open the message and click the link yourself in a moment.
  • Return to the Emails page in your Resend dashboard.
  • Refresh the page.
  • Confirm the newest entry is your Metrics click test email with a Delivered status.

No new entry in the log?

  • If the terminal returned an error instead of an id, check that the sender address matches the one that worked in your last step.
  • If the error mentions your key, copy it again from the API Keys list in your dashboard and rerun the command.
  • Why did my Resend send request fail?
Open the email and click the link

An open is recorded when your mail client loads a tiny tracking image inside the message. A click is recorded when you follow the rewritten link.

  • Open the inbox you sent the test emails to.
  • Open the Metrics click test email.
  • Allow remote images if your mail client asks you to.
  • Click the Visit NextWork link inside the email.
  • Open the earlier test emails in the same inbox so they register opens too.

Why your mail client changes the numbers

Open tracking depends on your mail client loading that image. Gmail and Apple Mail have privacy settings that fetch or block images on your behalf.

That is why open rate is the softest number in the whole dataset. Click rate is far harder to fake, because someone has to actually follow the link.

Read the events in your Emails log

Before you refresh the log, take a guess. How many separate events do you expect to see attached to the email you just opened and clicked?

  • Return to the Emails page in your Resend dashboard.
  • Refresh the page.
  • Click the Metrics click test entry to open its detail view.

The message now carries three events instead of one. Alongside the delivered event, you should see an opened event and a clicked event with their own timestamps.

Those three timestamps are exactly what the metrics endpoint counts up for you in the next step.

No opened or clicked events showing?

  • Events can take a moment to attach. Give it a minute and refresh the page again before assuming something broke.
  • Open and click tracking is disabled by default for all domains. You turn it on from the Domains page in your dashboard, under the Configuration tab, for a domain you have verified.
  • If your mail client blocked remote images, the open never fired. The click still counts, so check for that event first.
  • Why is my Resend email missing its opened event?

Your log now holds real engagement, not just deliveries. Next up, you'll pull all of it back as JSON with a single call to the metrics endpoint.

Pull Your Email Metrics

Your test emails are sent, opened and clicked, and the Resend Emails log has the events to prove it.

Reading events one row at a time works for five emails. It falls apart the moment you send five thousand.

The Email Metrics API rolls that whole log into one set of numbers. In this step you make a single authenticated call and read your rates straight back.

In this step, get ready to:
  • Call the Email Metrics endpoint with your API key.
  • Pin the call to a fixed start_date and end_date.
  • Read your delivery rate, open rate and bounce rate out of the response.
Call the metrics endpoint for the first time

The metrics endpoint sits at the same address as the send endpoint you already used, with /metrics on the end. Called with no parameters, it hands back every metric it has for the last 7 days.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal and press Enter to open it.
  • Pick the tab below that matches your system.

macOS

  • Replace your-api-key-here with the API key you saved earlier.
  • Get every metric for the last 7 days by running this command:
curl -X GET 'https://api.resend.com/emails/metrics' -H 'Authorization: Bearer your-api-key-here'

Windows

  • Replace your-api-key-here with the API key you saved earlier.
  • Get every metric for the last 7 days by running this command:
curl.exe -X GET 'https://api.resend.com/emails/metrics' -H 'Authorization: Bearer your-api-key-here'

What does this command do?

  • -X GET asks the API to hand data back rather than create anything.
  • https://api.resend.com/emails/metrics is the Email Metrics endpoint.
  • The Authorization header proves the request is yours, using the same key you sent email with.
  • With no query parameters attached, the API falls back to the last 7 days and returns all 22 metrics.
  • Read the block of JSON that prints in your terminal.

It opens with "object": "metrics" and finishes with a totals object holding every count and rate for the window.

Getting an error or a wall of zeros?

  • If the response is an error about your key rather than a block of numbers, paste the key again and check for stray spaces inside the quotes.
  • If every number is 0, give it a few minutes. Metrics aggregate on a short delay, so they can lag behind the Emails log.

My Resend metrics call is not returning what I expected

Scroll the command itself off-screen before you screenshot, or blur it. Your API key should never appear in an upload.

Pin the call to a date range

That default window slides. Run the same command next week and the numbers shift underneath you.

  • Pick a start date from about a week before today, written as an ISO 8601 date like 2026-08-20.
  • Use today's date as the end date, in the same format.
  • Fix the window to your own two dates by running the command for your system:

macOS

curl -X GET 'https://api.resend.com/emails/metrics?start_date=[[START_DATE="2026-08-20"]]&end_date=[[END_DATE="2026-08-27"]]' -H 'Authorization: Bearer your-api-key-here'

Windows

curl.exe -X GET 'https://api.resend.com/emails/metrics?start_date=[[START_DATE="2026-08-20"]]&end_date=[[END_DATE="2026-08-27"]]' -H 'Authorization: Bearer your-api-key-here'

What do the date parameters do?

  • start_date sets the earliest moment counted. It can reach back as far as your account retains data.
  • end_date sets the latest moment counted. A date in the future gets clamped to now.
  • Without dimensions, the whole window collapses into one totals object. That is exactly what you want before you split it by day.
  • Compare the start_date field at the top of the response with the date you typed.

The API echoes both of your dates back with a timestamp attached. Your window is fixed now, and it stays fixed however long you leave the command sitting there.

Response complaining about the dates?

  • Check both dates are plain calendar dates like 2026-08-20 with no time on the end.
  • Check the & between the two parameters survived the paste, and the whole URL is still inside one pair of single quotes.

Why is my date-filtered metrics request failing?

Read your delivery, open and bounce rates

Where do the rates come from?

The raw counts are the events themselves. delivered, opened and bounced each tick up once per event Resend recorded.

The rates are percentages Resend derives from those counts for you. That saves you doing the division in your chart later.

  • Find delivered, opened and clicked inside the totals object.
  • Find delivery_rate, open_rate and bounce_rate further down the same object.

Your opened count should be close to the number of test emails you opened by hand, and bounce_rate should be sitting at zero.

One question is still open. Are those numbers genuinely scoped to the dates you passed, or is the API just handing back everything it has?

Before you run anything, make a call. If you ask for a week that ended before your Resend account existed, what do you think sent and open_rate will come back as?

  • Find the date range in the command you just ran:
?start_date=[[START_DATE="2026-08-20"]]&end_date=[[END_DATE="2026-08-27"]]
  • Swap both dates for a week from before you signed up, so the fragment reads:
?start_date=[[PAST_START_DATE="2026-01-01"]]&end_date=[[PAST_END_DATE="2026-01-07"]]
  • Run the edited command.

Every number in totals comes back as 0. The dates really are filtering, which means the numbers you read a moment ago were genuinely yours.

  • Put your original two dates back and run the command one final time.

Your counts and rates return. That is the exact request your chart will run.

Old window still showing your counts?

  • Check you changed both dates rather than only the first one.
  • If your test emails went out close to midnight, they can land in a neighbouring day. Widen the window by a day and run it again.

Why are my metrics ignoring the date range I passed?

One call, one set of totals for the whole window. Next up, you'll split that same call into one row per day so there's a shape to plot.

Group Metrics by Day

Last step you pulled real numbers out of the Resend Email Metrics API. One response came back holding your delivery, open and bounce rates for the whole date range.

A single total hides the shape of your sending. You cannot tell whether your bounce rate spiked yesterday or whether it has been creeping up all week.

The same endpoint can break that total into buckets. In this step you turn one aggregated row into one row per day.

In this step, get ready to:
  • Add the granularity parameter to your metrics call and watch what it changes.
  • Add the period dimension so the response splits into daily rows.
  • Pin those daily buckets to your own timezone.
Add the granularity parameter on its own

Resend buckets time with a parameter called granularity. Setting it to daily asks for one bucket per day.

  • Switch back to the terminal where you ran your metrics call.

Before you run anything, make a call: do you think asking for daily buckets is enough on its own to split the response into days?

  • Ask for daily buckets by running this command:
curl -X GET 'https://api.resend.com/emails/metrics?start_date=[[START_DATE="2026-08-21"]]&end_date=[[END_DATE="2026-08-27"]]&granularity=daily' \
     -H 'Authorization: Bearer re_your_api_key_here'

What does this command do?

  • start_date and end_date bound the range you want numbers for, written as ISO 8601 dates.
  • granularity=daily asks Resend to bucket that range one day at a time.
  • The Authorization header carries your API key so Resend knows whose account to read.

The response still holds a single totals object and no data array. Nothing has been split by day.

That is the expected result. Resend accepts granularity and quietly ignores it until something else asks for the data to be grouped over time.

Not seeing JSON come back?

  • Check the key in the Authorization header is the one you saved earlier, with Bearer in front of it.
  • Make sure the whole URL stays inside the single quotes, otherwise your shell splits the command at the first &.

Why is my metrics request not returning JSON?

Group the response by period

Grouping comes from a second parameter called dimensions. It defaults to an empty list, which is why every response so far has been one lump total.

  • Break the range into time buckets by running this command:
curl -X GET 'https://api.resend.com/emails/metrics?start_date=[[START_DATE="2026-08-21"]]&end_date=[[END_DATE="2026-08-27"]]&granularity=daily&dimensions=period' \
     -H 'Authorization: Bearer re_your_api_key_here'

Why period and granularity work as a pair

dimensions decides how the data is split. period splits it over time, in chronological order.

granularity then decides how wide each of those time buckets is. That is why it did nothing on its own a moment ago.

This time the response carries a data array alongside totals.

Each entry in that array carries its own period date plus its own delivery_rate, open_rate and bounce_rate.

Only seeing one entry in the data array?

  • Your test emails may all have landed on the same day, so only one day has activity to report.
  • Resend caches metrics responses for up to 15 minutes, so a range you just queried can come back slightly stale.
  • Widen the range by moving start_date further back and run the command again.

Why does my grouped response only have one row?

Pin the buckets to your own timezone

Resend has to decide which day each event belongs to, and it can only do that against a timezone. Leave it unset and an email you opened late last night can land in the wrong bucket.

Before you run the next command, predict the result: will the number of entries stay the same, or will some counts move between days?

  • Bucket the days against your own clock by running this command:
curl -X GET 'https://api.resend.com/emails/metrics?start_date=[[START_DATE="2026-08-21"]]&end_date=[[END_DATE="2026-08-27"]]&granularity=daily&dimensions=period&timezone=[[YOUR_TIMEZONE="America/New_York"]]' \
     -H 'Authorization: Bearer re_your_api_key_here'

You get the same data array back, one entry per bucket, with each period date now matching your local calendar.

If you sent any test emails late in the evening, their counts shift to the day you actually pressed send.

Resend rejecting your timezone?

  • IANA names are case-sensitive and use an underscore instead of a space, like America/New_York.
  • Abbreviations such as EST are not IANA names, so pick the full region and city pair instead.

Which IANA timezone name should I be using?

✔️ Awesome, I've got everything!

Great. Your metrics call now returns a daily series instead of a single total.

ⓧ I'd like to double check the full command

curl -X GET 'https://api.resend.com/emails/metrics?start_date=[[START_DATE="2026-08-21"]]&end_date=[[END_DATE="2026-08-27"]]&granularity=daily&dimensions=period&timezone=[[YOUR_TIMEZONE="America/New_York"]]' \
     -H 'Authorization: Bearer re_your_api_key_here'

Your numbers now have a shape over time. Next up, you'll draw that shape as a chart and add a warning line that fires when your bounce rate climbs too high.

Chart the Rates and Add a Bounce Warning

Your metrics call now returns one row per day, each with its own open rate and bounce rate. Reading that back as raw JSON works, but a wall of numbers hides the trend you actually care about.

A chart makes the shape obvious in a second. In this step you'll build a plain HTML page that draws your open rate and bounce rate as two lines, then prints a warning whenever a day's bounce rate goes over a limit you set.

In this step, get ready to:
  • Build a local HTML page that holds your daily metrics.
  • Draw your open rate and bounce rate as two lines on a chart.
  • Print a warning line whenever a day's bounce rate crosses your limit.
Create the page and paste in your daily numbers

The page is a single text file that your browser opens straight from disk. No build tools, no server, no packages.

  • Move to your Desktop and create your project folder by running these commands:
cd ~/Desktop
mkdir email-metrics
cd email-metrics
  • Confirm you're inside the new folder by running:
pwd
  • Check that the output ends with email-metrics.
  • Press **Cmd+Space** (macOS) or the **Windows** key (Windows) to open your search bar.
  • Type VS Code and press Enter to open it.
  • Click File in the top menu bar.
  • Click Open Folder.
  • Select the email-metrics folder you just created, then click Open.

Why paste the numbers instead of fetching them?

Browser JavaScript cannot call the Resend API. The API deliberately sends no Access-Control-Allow-Origin header, so the browser blocks the request before it leaves your machine.

That block is a guardrail, not a bug. An API key sitting in a page anyone can view-source is a key anyone can send mail with.

So the terminal keeps the key and does the calling. The page only ever sees the numbers that come back.

  • In the Explorer sidebar, click the New File icon.
  • Type index.html and press Enter.
  • Build the shell of your page by pasting this into index.html:
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Email Metrics</title>
  </head>
  <body>
    <h1>Email open and bounce rates</h1>
    <p>
      <span style="color: #2f7d32">Open rate</span>
      <span style="color: #c62828">Bounce rate</span>
    </p>
    <svg id="chart" width="640" height="280" style="border: 1px solid #ddd"></svg>
    <div id="warnings"></div>
    <script>
      // Your daily rows from the metrics API go in here
      const dailyMetrics = [];
    </script>
  </body>
</html>

What does this code do?

  • The <svg id="chart"> element is the empty box your two lines get drawn into.
  • The <div id="warnings"> element is where bounce warnings get printed later.
  • The two coloured spans are a legend so you can tell the lines apart.
  • The dailyMetrics array starts empty. Your own rows go in next.
  • Save index.html.
  • Open the email-metrics folder in Finder (macOS) or File Explorer (Windows).
  • Double-click index.html to open it in your browser.
  • Check that you see the heading Email open and bounce rates, the two-colour legend, and an empty bordered box below it.

Seeing the code itself instead of a page?

The file was probably saved without the .html ending, so your browser treats it as plain text. Rename it in the VS Code sidebar and reopen it.

A completely blank page usually means an older empty file got opened. Check the browser address bar ends with index.html.

My HTML page opens but shows nothing at all. What should I check first?

  • Return to the terminal where you called the metrics endpoint, and run that request again.
  • Copy the data array from the response, including the square brackets.
  • In index.html, find this line:
const dailyMetrics = [];
  • Replace the empty brackets with your own rows, so the line now looks like this:
const dailyMetrics = [
  { "period": "2026-08-25", "open_rate": 100, "bounce_rate": 0 },
  { "period": "2026-08-26", "open_rate": 66.7, "bounce_rate": 0 },
  { "period": "2026-08-27", "open_rate": 50, "bounce_rate": 0 }
];

Your rows will look longer than this

The example above trims each day down to three fields so it fits on the page. Your real rows carry every metric the endpoint returns, and that is fine.

The chart only reads the fields it needs. Everything else rides along untouched.

  • Save index.html.
  • Switch to your browser and refresh the page.
  • Press **Cmd+Option+J** (macOS) or **Ctrl+Shift+J** (Windows) to open the browser console.
  • Type dailyMetrics.length and press Enter.
  • Check that the number matches how many days came back in your response, such as 3.

Console showing a syntax error?

Check you pasted only the data array and not the whole response body. The surrounding braces and the other top-level fields will break the line.

Also check the closing ]; survived the paste. A missing bracket takes the whole script down with it.

Why is my browser console throwing a syntax error after I pasted JSON into a JavaScript array?

Draw the open rate and bounce rate lines

A rate is a percentage. A chart is pixels. Something has to translate one into the other before anything can be drawn.

  • Turn your rates into chart coordinates by adding this below your dailyMetrics array, still inside the <script> tag:
const width = 640;
const height = 280;
const padding = 40;

// Turn one day's rate into an x,y point inside the chart box
function pointsFor(key) {
  const stepX = (width - padding * 2) / Math.max(dailyMetrics.length - 1, 1);
  return dailyMetrics
    .map(function (row, i) {
      const x = padding + i * stepX;
      const y = height - padding - (row[key] / 100) * (height - padding * 2);
      return x + "," + y;
    })
    .join(" ");
}

What does this code do?

  • The width and height constants match the box you already put in your HTML.
  • The padding constant keeps the lines off the edges so they stay readable.
  • The stepX value spreads your days evenly from left to right.
  • Dividing the rate by 100 maps a percentage onto the height of the box, so 100% sits at the top and 0% sits on the floor.
  • Save index.html and refresh your browser.
  • In the console, type pointsFor("open_rate") and press Enter.
  • Check that you get back a string of coordinate pairs, one pair per day.

Getting NaN in the coordinates?

That means the rate field name doesn't match your data. Expand one row in the console and check whether your response spells it open_rate.

Why do my calculated chart coordinates come out as NaN?

  • Draw the two lines by adding this below your pointsFor function:
const chart = document.getElementById("chart");

// Draw one metric as a coloured line across the chart
function drawLine(key, colour) {
  const line = document.createElementNS("http://www.w3.org/2000/svg", "polyline");
  line.setAttribute("points", pointsFor(key));
  line.setAttribute("fill", "none");
  line.setAttribute("stroke", colour);
  line.setAttribute("stroke-width", "2");
  chart.appendChild(line);
}

drawLine("open_rate", "#2f7d32");
drawLine("bounce_rate", "#c62828");

What does this code do?

  • A polyline is the SVG shape that joins a list of points into one continuous line.
  • The createElementNS call is the SVG version of creating an element. The plain version produces a shape the browser refuses to draw.
  • Setting fill to none keeps it a line rather than a filled blob.
  • The two calls at the bottom run the same drawing logic twice, once per metric.
  • Save index.html and refresh your browser.
  • Check that a green open rate line now runs across the box, with a red bounce rate line below it.

If every test email landed, your red line sits flat along the floor of the chart. That flat line is the good outcome.

Chart still empty?

Check that drawLine sits below pointsFor in the file. The two calls at the bottom run immediately, so anything they depend on has to exist first.

A single day in your range gives you one point, and one point draws no visible line. Widen your date range and re-copy the response.

Why is my SVG polyline not showing up on the page?

Warn when the bounce rate crosses your limit

A chart tells you something is wrong once you look at it. A warning tells you without being asked.

Pick the bounce rate you're willing to tolerate before you want to know about it. We'll use 5%.

  • Add the bounce check by pasting this below your drawLine calls:
const threshold = 5;
const warnings = document.getElementById("warnings");

// Print one line for every day that went over the limit
dailyMetrics
  .filter(function (row) {
    return row.bounce_rate > threshold;
  })
  .forEach(function (row) {
    const line = document.createElement("p");
    line.textContent = "Bounce rate hit " + row.bounce_rate + "% on " + row.period + " (limit " + threshold + "%).";
    line.style.color = "#c62828";
    warnings.appendChild(line);
  });

What does this code do?

  • The threshold constant is the only number you tune. Everything else reads from it.
  • The filter keeps only the days that went over the limit.
  • Each surviving day gets its own red line inside the warnings area, naming the rate and the date.

Before you refresh, do you think a warning line will appear? Think about what happened to the test emails you sent earlier.

  • Save index.html and refresh your browser.

Most likely nothing new appears. Every test email you sent reached your own inbox, so no day came anywhere near 5%.

An empty warning area could mean the check works, or that the code never ran at all. Force it to fire so you know which one you have.

  • In index.html, find this line:
const threshold = 5;
  • Change 5 to -1 so every day counts as over the limit:
const threshold = -1;
  • Save index.html and refresh your browser.
  • Check that one red warning line now appears under the chart for every day in your range.

That proves the wiring end to end. The check reads your data, compares it, and writes to the page.

  • Find this line again:
const threshold = -1;
  • Put your real limit back by changing -1 to 5:
const threshold = 5;
  • Save index.html and refresh your browser.
  • Check that the warning lines have gone and your two rate lines are still drawn.

No warning lines even at -1?

Open the console and look for an error above your check. Anything that throws earlier in the script stops every line below it from running.

If there's no error, confirm your rows really do use bounce_rate and period as their field names, and update the code to match yours if they differ.

Why does my filter and forEach produce no elements on the page?

✔️ Awesome, I've got everything!

Great. Double check you've saved index.html one last time.

ⓧ I'd like to double check the full code

Scroll up and confirm each of these blocks is present, in this order:

  • The page skeleton with <svg id="chart"> and <div id="warnings">.
  • Your own daily rows pasted into dailyMetrics.
  • The width, height and padding constants, followed by the pointsFor function.
  • The chart reference, the drawLine function, and its two calls.
  • The threshold constant set back to 5, and the bounce check below it.

Everything from the second block onwards lives inside the one <script> tag.

Your page now reads your own sending data and speaks up when the bounce rate needs attention. Point it at a real sending domain later and it keeps working unchanged.

Secret mission

Add a Daily Highlights Readout to Your Page

Your chart shows the shape of your sending, but it takes squinting to read an exact number off it. Pull your best open rate day and your worst bounce rate day straight out of the daily rows and print them under the chart.

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. Your chart page runs entirely from your laptop, so the only thing worth thinking about is the API key sitting in your script.

Your API key is still live

The key you saved is a working credential. Anyone who gets hold of it can send email from your account and read your metrics.

If you are finished with this project, revoking the key in the dashboard is the safest option. You can always generate a new one later.

Resources you used:

  • Your Resend API key, generated under API Keys in the dashboard.
  • Your local project folder containing index.html and the chart script.
  • The test emails sitting in your own inbox and in the Resend Emails log.

Keep everything running

No action needed. Choose this if you're still actively building on your chart page or want to keep watching your bounce rate.

  • Leave your API key active so index.html keeps returning metrics.
  • Send yourself another test email in a few days, then reopen the page to watch a new day appear on the chart.

Keep your key out of version control

Your key is pasted directly into index.html. If you ever push this folder to a public repo, that key goes public with it.

Move the key into a file you exclude from git before sharing the project with anyone.

Pause - I'll come back to this later

Stop the key from working while keeping your page and your data. Choose this if you want to return to the project without leaving a live credential around.

  • In the Resend dashboard, open the API Keys page.
  • Delete the key you created for this project.
  • Keep your project folder and index.html exactly where they are.

Your Emails log and metrics history stay in your Resend account. Generating a fresh key and pasting it back into the page brings the chart straight back.

Delete - I don't want to use this again

Remove all project resources and start fresh. Choose this if you're done with the chart and the key.

  • In the Resend dashboard, open the API Keys page.
  • Delete the key you created for this project.
  • Confirm the key no longer appears in the list.
  • Delete your local project folder by running these commands:

macOS

cd ~/Desktop
rm -rf [[PROJECT_FOLDER="your project folder name"]]

Windows

cd ~/Desktop
rmdir /s /q [[PROJECT_FOLDER="your project folder name"]]
  • Confirm the folder is gone by running ls and checking it no longer appears.
  • Delete the test emails from your own inbox if you don't want them cluttering it.

Still seeing your project folder?

Check that the folder name you typed matches the one on your Desktop exactly, including capitalisation.

If the name has a space in it, wrap it in quotes so the shell treats it as one argument. Why won't my folder delete from the terminal?

Nice Work!

Nice Work

Nice work! You've built a local analytics page that pulls real numbers out of Resend and turns them into a chart you can actually read.

Every number on that chart came from mail you sent, opened, and clicked yourself.

You've learned how to:

  • Send real test emails with the Resend API and trace each one through the Emails log until its delivered, opened, and clicked events show up.
  • Pull delivery, open, and bounce rates back through a single metrics call, then group them by day so you get one row per day instead of one lifetime total.
  • Turn that daily series into a line chart with a bounce rate warning that flags any day your threshold gets crossed.
  • Secret Mission: Take on the optional challenge to push your email analytics skills further.

Ready to quiz yourself?