Idempotency Keys
Include an idempotency key in any email requests to ensure that the same email request is processed only once, even if it's sent multiple times.
How it works
Section titled “How it works”When you send an email with an idempotency key, Resend checks whether an email with the same idempotency key has already been sent in the last 24 hours. This is an optional feature that simplifies managing retries on your side.
This makes it safe to retry requests that send an email. You don't have to worry about checking if the original request was sent. Instead, you can make the same request and our API will give the same response, without actually sending the email again.
How to use idempotency keys
Section titled “How to use idempotency keys”Idempotency keys can be up to 256 characters and must be unique per API request.
We recommend that you use a UUID or other string that uniquely identifies that specific email.
Send the key in the Idempotency-Key HTTP header in your API requests. Our SDKs also provide a convenient way to set this header. If you're using SMTP, you can set the Resend-Idempotency-Key email header instead.
Idempotency keys are kept in the system for 24 hours. This gives you an ample window to retry any failed processes on your end without having to keep track of the sent status.
POST /emails endpoint example
Section titled “POST /emails endpoint example”import { Resend } from 'resend';const resend = new Resend('re_xxxxxxxxx');await resend.emails.send( { from: 'Acme <onboarding@resend.dev>', to: ['delivered@resend.dev'], subject: 'hello world', html: '<p>it works!</p>', }, { idempotencyKey: 'welcome-user/123456789', },);$resend = Resend::client('re_xxxxxxxxx');$resend->emails->send([ 'from' => 'Acme <onboarding@resend.dev>', 'to' => ['delivered@resend.dev'], 'subject' => 'hello world', 'html' => '<p>it works!</p>',], [ 'idempotency_key' => 'welcome-user/123456789',]);import resendresend.api_key = "re_xxxxxxxxx"params: resend.Emails.SendParams = { "from": "Acme <onboarding@resend.dev>", "to": ["delivered@resend.dev"], "subject": "hello world", "html": "<p>it works!</p>"}options: resend.Emails.SendOptions = { "idempotency_key": "welcome-user/123456789",}resend.Emails.send(params, options)require "resend"Resend.api_key = "re_xxxxxxxxx"params = { "from": "Acme <onboarding@resend.dev>", "to": ["delivered@resend.dev"], "subject": "hello world", "html": "<p>it works!</p>"}Resend::Emails.send( params, options: { idempotency_key: "welcome-user/123456789" })package mainimport ( "context" "github.com/resend/resend-go/v4")func main() { ctx := context.TODO() client := resend.NewClient("re_xxxxxxxxx") params := &resend.SendEmailRequest{ From: "onboarding@resend.dev", To: []string{"delivered@resend.dev"}, Subject: "hello world", Html: "<p>it works!</p>", } options := &resend.SendEmailOptions{ IdempotencyKey: "welcome-user/123456789", } _, err := client.Emails.SendWithOptions(ctx, params, options) if err != nil { panic(err) }}use resend_rs::types::CreateEmailBaseOptions;use resend_rs::{Resend, Result};#[tokio::main]async fn main() -> Result<()> { let resend = Resend::new("re_xxxxxxxxx"); let from = "Acme <onboarding@resend.dev>"; let to = ["delivered@resend.dev"]; let subject = "Hello World"; let email = CreateEmailBaseOptions::new(from, to, subject) .with_html("<p>it works!</p>") .with_idempotency_key("welcome-user/123456789"); let _email = resend.emails.send(email).await?; Ok(())}import com.resend.*;import com.resend.core.net.RequestOptions;import com.resend.services.emails.model.CreateEmailOptions;import com.resend.services.emails.model.CreateEmailResponse;Resend resend = new Resend("re_xxxxxxxxx");CreateEmailOptions params = CreateEmailOptions.builder() .from("Acme <onboarding@resend.dev>") .to("delivered@resend.dev") .subject("hello world") .html("<p>it works!</p>") .build();RequestOptions options = RequestOptions.builder() .setIdempotencyKey("welcome-user/123456789").build();CreateEmailResponse data = resend.emails().send(params, options);using Resend;IResend resend = ResendClient.Create( "re_xxxxxxxxx" ); // Or from DIvar key = IdempotencyKey.New<int>( "welcome-user", 123456789 );var resp = await resend.EmailSendAsync(key, new EmailMessage(){ From = "Acme <onboarding@resend.dev>", To = "delivered@resend.dev", Subject = "hello world", HtmlBody = "<p>it works!</p>",} );Console.WriteLine( "Email Id={0}", resp.Content );curl -X POST 'https://api.resend.com/emails' \ -H 'Authorization: Bearer re_xxxxxxxxx' \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: welcome-user/123456789' \ -d $'{ "from": "Acme <onboarding@resend.dev>", "to": ["delivered@resend.dev"], "subject": "hello world", "html": "<p>it works!</p>"}'resend emails send \ --from "Acme <onboarding@resend.dev>" \ --to delivered@resend.dev \ --subject "hello world" \ --html "<p>it works!</p>" \ --idempotency-key "welcome-user/123456789"From: Acme <onboarding@resend.dev>To: delivered@resend.devSubject: hello worldResend-Idempotency-Key: welcome-user/123456789<p>it works!</p>POST /emails/batch endpoint example
Section titled “POST /emails/batch endpoint example”import { Resend } from 'resend';const resend = new Resend('re_xxxxxxxxx');await resend.batch.send( [ { from: 'Acme <onboarding@resend.dev>', to: ['foo@gmail.com'], subject: 'hello world', html: '<h1>it works!</h1>', }, { from: 'Acme <onboarding@resend.dev>', to: ['bar@outlook.com'], subject: 'world hello', html: '<p>it works!</p>', }, ], { idempotencyKey: 'team-quota/123456789', },);$resend = Resend::client('re_xxxxxxxxx');$resend->batch->send( [ [ 'from' => 'Acme <onboarding@resend.dev>', 'to' => ['foo@gmail.com'], 'subject' => 'hello world', 'html' => '<h1>it works!</h1>', ], [ 'from' => 'Acme <onboarding@resend.dev>', 'to' => ['bar@outlook.com'], 'subject' => 'world hello', 'html' => '<p>it works!</p>', ] ], [ 'idempotency_key' => 'team-quota/123456789', ]);import resendfrom typing import Listresend.api_key = "re_xxxxxxxxx"params: List[resend.Emails.SendParams] = [ { "from": "Acme <onboarding@resend.dev>", "to": ["foo@gmail.com"], "subject": "hello world", "html": "<h1>it works!</h1>", }, { "from": "Acme <onboarding@resend.dev>", "to": ["bar@outlook.com"], "subject": "world hello", "html": "<p>it works!</p>", }]options: resend.Batch.SendOptions = { "idempotency_key": "team-quota/123456789",}resend.Batch.send(params, options)require "resend"Resend.api_key = 're_xxxxxxxxx'params = [ { "from": "Acme <onboarding@resend.dev>", "to": ["foo@gmail.com"], "subject": "hello world", "html": "<h1>it works!</h1>", }, { "from": "Acme <onboarding@resend.dev>", "to": ["bar@outlook.com"], "subject": "world hello", "html": "<p>it works!</p>", }]Resend::Batch.send( params, options: { idempotency_key: "team-quota/123456789" })package mainimport ( "context" "fmt" "github.com/resend/resend-go/v4")func main() { ctx := context.TODO() client := resend.NewClient("re_xxxxxxxxx") var batchEmails = []*resend.SendEmailRequest{ { From: "Acme <onboarding@resend.dev>", To: []string{"foo@gmail.com"}, Subject: "hello world", Html: "<h1>it works!</h1>", }, { From: "Acme <onboarding@resend.dev>", To: []string{"bar@outlook.com"}, Subject: "world hello", Html: "<p>it works!</p>", }, } options := &resend.BatchSendEmailOptions{ IdempotencyKey: "team-quota/123456789", } sent, err := client.Batch.SendWithOptions(ctx, batchEmails, options) if err != nil { panic(err) } fmt.Println(sent.Data)}use resend_rs::{idempotent::IdempotentTrait, types::CreateEmailBaseOptions};use resend_rs::{Resend, Result};#[tokio::main]async fn main() -> Result<()> { let resend = Resend::new("re_xxxxxxxxx"); let emails = vec![ CreateEmailBaseOptions::new( "Acme <onboarding@resend.dev>", vec!["foo@gmail.com"], "hello world", ) .with_html("<h1>it works!</h1>"), CreateEmailBaseOptions::new( "Acme <onboarding@resend.dev>", vec!["bar@outlook.com"], "world hello", ) .with_html("<p>it works!</p>"), ] .with_idempotency_key("team-quota/123456789"); let _emails = resend.batch.send(emails).await?; Ok(())}import com.resend.*;import com.resend.core.exception.ResendException;import com.resend.core.net.RequestOptions;import com.resend.services.batch.model.CreateBatchEmailsResponse;import com.resend.services.emails.model.CreateEmailOptions;import java.util.Arrays;public class Main { public static void main(String[] args) throws ResendException { Resend resend = new Resend("re_xxxxxxxxx"); CreateEmailOptions firstEmail = CreateEmailOptions.builder() .from("Acme <onboarding@resend.dev>") .to("foo@gmail.com") .subject("hello world") .html("<h1>it works!</h1>") .build(); CreateEmailOptions secondEmail = CreateEmailOptions.builder() .from("Acme <onboarding@resend.dev>") .to("bar@outlook.com") .subject("world hello") .html("<p>it works!</p>") .build(); CreateBatchEmailsResponse data = resend.batch().send( Arrays.asList(firstEmail, secondEmail), RequestOptions.builder().setIdempotencyKey("team-quota/123456789").build() ); }}using Resend;IResend resend = ResendClient.Create( "re_xxxxxxxxx" ); // Or from DIvar key = IdempotencyKey.New<int>( "team-quota", 123456789 );var mail1 = new EmailMessage(){ From = "Acme <onboarding@resend.dev>", To = "foo@gmail.com", Subject = "hello world", HtmlBody = "<p>it works!</p>",};var mail2 = new EmailMessage(){ From = "Acme <onboarding@resend.dev>", To = "bar@outlook.com", Subject = "hello world", HtmlBody = "<p>it works!</p>",};var resp = await resend.EmailBatchAsync(key, [ mail1, mail2 ] );Console.WriteLine( "Nr Emails={0}", resp.Content.Count );curl -X POST 'https://api.resend.com/emails/batch' \ -H 'Authorization: Bearer re_xxxxxxxxx' \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: team-quota/123456789' \ -d $'[ { "from": "Acme <onboarding@resend.dev>", "to": ["foo@gmail.com"], "subject": "hello world", "html": "<h1>it works!</h1>" }, { "from": "Acme <onboarding@resend.dev>", "to": ["bar@outlook.com"], "subject": "world hello", "html": "<p>it works!</p>" }]'Possible responses
Section titled “Possible responses”After checking if an email with the same idempotency key has already been sent, Resend returns one of the following responses:
- Successful responses will return the email ID of the sent email.
- Error responses will return one of the following errors:
400:invalid_idempotency_key- the idempotency key has to be between 1–256 characters. You can retry with a valid key or without supplying an idempotency key.409:invalid_idempotent_request- this idempotency key has already been used on a request that had a different payload. Retrying this request is useless without changing the idempotency key or payload.409:concurrent_idempotent_requests- another request with the same idempotency key is in progress. As it isn't finished yet, Resend can't return its original response, but it is safe to retry this request later if needed.