---
title: "Customer.io to Resend"
slug: customer-io
description: "A guide on how to quickly switch from Customer.io to Resend."
image: /static/migrate/customer-io-og.jpg
hero_image: /static/migrate/customer-io-resend.png
updated_at: "2024-11-28"
---

## Introduction

If you're considering migrating from Customer.io to Resend, you're in the right place.

This guide will help you understand the key differences between the two services and provide you with the necessary steps to make the transition as smooth as possible.

**Jump ahead**

- **[History](#history)**
- **[Concepts](#concepts)**
- **[Official SDKs](#official-sdks)**
- **[Send email via API](#send-email-via-api)**
- **[Send email via SMTP](#send-email-via-smtp)**
- **[Webhooks](#webhooks)**
- **[Security & Privacy](#security-privacy)**
- **[Idempotency Keys](#idempotency-keys)**
- **[Additional Features](#additional-features)**
- **[Pricing](#pricing)**
- **[Conclusion](#conclusion)**

## History

Customer.io and Resend are both email delivery services, but they have different histories and focuses.

**Key differences**

- Customer.io was founded in 2012 with a focus on product and marketing email.
- Resend was founded in 2023. Resend has a focus on providing a modern developer experience.

## Concepts

Both Customer.io and Resend provide user-friendly dashboards for managing your email sending.

**Domains**

In Customer.io and Resend you need to access the _Domains_ page to verify your domain.

With Resend, domains are verified on the _Domains_ page.

<img
  src="/static/product-pages/screenshot-domain.png"
  alt="Resend Domains page"
  className="extraWidth"
/>

Both Customer.io and Resend offer similar authentication features.

<Table
  data={{
    headers: ["Name", "Customer.io", "Resend"],
    rows: [
      ["DKIM", "DKIM enforced", "DKIM enforced"],
      ["SPF", "SPF enforced", "SPF enforced"],
      ["DMARC", "DMARC required", "DMARC recommended"],
    ]
  }}
/>

**Activity Feed**

When you send email through Customer.io, sent email is visible on the _Deliveries & Drafts_ page.

When you send email with Resend, emails are visible on the _Emails_ page.

<img
  src="/static/product-pages/screenshot-emails.png"
  alt="Resend Emails page"
  className="extraWidth"
/>

**Dashboard**

Customer.io shows sending statistics on the _Analysis_ page.

In Resend, statistics are shown on the _Metrics_ page.

<img
  src="/static/product-pages/screenshot-metrics.png"
  alt="Resend Metrics page"
  className="extraWidth"
/>

## Official SDKs

Both Customer.io and Resend provide robust integration options, making it easy to integrate into your application.

**Key differences**

- Customer.io does not offer offical backend SDKs, relying instead on CDP-like integration sources.
- Resend offers a variety of official backend SDKs for popular programming languages.

<Table
  data={{
    headers: ["Platform", "Customer.io", "Resend"],
    rows: [
      [
        "Node.js",
        {
          href: "https://docs.customer.io/integrations/data-in/connections/servers/node/",
          text: "node.js source"
        },
        { href: "https://github.com/resend/resend-node", text: "resend-node" }
      ],
      [
        "PHP",
        "-",
        { href: "https://github.com/resend/resend-php", text: "resend-php" }
      ],
      [
        "Python",
        {
          href: "https://docs.customer.io/integrations/data-in/connections/servers/python/",
          text: "python source"
        },
        {
          href: "https://github.com/resend/resend-python",
          text: "resend-python"
        }
      ],
      [
        "Ruby",
        "-",
        {
          href: "https://github.com/resend/resend-ruby",
          text: "resend-ruby"
        }
      ],
      [
        "Go",
        {
          href: "https://docs.customer.io/integrations/data-in/connections/servers/go/",
          text: "go source"
        },
        {
          href: "https://github.com/resend/resend-go",
          text: "resend-go"
        }
      ],
      [
        "Rust",
        "-",
        { href: "https://github.com/resend/resend-rust", text: "resend-rust" }
      ],
      [
        "Java",
        "-",
        { href: "https://github.com/resend/resend-java", text: "resend-java" }
      ],
      [
        ".NET",
        "-",
        { href: "https://github.com/resend/resend-dotnet", text: "resend-dotnet" }
      ]
    ]
  }}
/>

## Send email via API

Both Customer.io and Resend provide a REST API for sending emails programmatically.

**Key differences**

_Rate limiting_

- Customer.io can significantly delay your messages if rate limits are exceeded, but doesn't provide more details or notify you of this delay in its API responses.
- Resend has response headers describing your current rate limit following every request in conformance with the IETF standard ([see docs](/docs/api-reference/introduction#rate-limit)).

_Logging_

- Customer.io's API only returns an HTTP response. There are no logs of API requests.
- Resend stores a history of API request logs, allowing for easy debugging of API responses and error codes ([see docs](/docs/api-reference/introduction#response-codes)).

_Data Storage Requirements_
- A user record is called a "Person" in Customer.io, and a "Contact" in Resend.
- Customer.io requires that every sent email is tied to a "Person/Contact" record.
- Resend only requires Contacts when using [Broadcasts](/docs/dashboard/broadcasts/introduction) or [Automations](/docs/dashboard/automations/introduction), but not for transactional emails.

**Transactional Email**

[Learn if your email qualifies as transactional email](/docs/transactional-vs-marketing-email). For all non-transactional automations, see the next section titled "Automations & Campaigns".

_Customer.io_

<CodeTabs>
```nodejs
const http = require("https");

const options = {
  "method": "POST",
  "hostname": "api.customer.io",
  "port": null,
  "path": "/v1/send/email",
  "headers": {
    "content-type": "application/json"
  }
};

const req = http.request(options, function (res) {
  const chunks = [];

  res.on("data", function (chunk) {
    chunks.push(chunk);
  });

  res.on("end", function () {
    const body = Buffer.concat(chunks);
    console.log(body.toString());
  });
});

req.write(JSON.stringify({
  transactional_message_id: 44,
  to: 'cool.person@example.com',
  from: 'override-templated-address@example.com',
  subject: 'Order receipt',
  identifiers: {email: 'cool.person@example.com'},
  message_data: {password_reset_token: 'abcde-12345-fghij-d888', account_id: '123dj'},
  attachments: {'file1.csv': 'base64encodedcontent', 'file2.pdf': 'base64encodedcontent'},
  headers: {'X-Mailgun-Tag': 'my-cool-tag'},
  bcc: 'bcc@example.com',
  disable_message_retention: false,
  send_to_unsubscribed: true,
  tracked: true,
  queue_draft: false,
  disable_css_preprocessing: true
}));
req.end();
```

```ruby
require 'uri'
require 'net/http'
require 'openssl'

url = URI("https://api.customer.io/v1/send/email")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.new(url)
request["content-type"] = 'application/json'
request.body = "{\"transactional_message_id\":44,\"to\":\"cool.person@example.com\",\"from\":\"override-templated-address@example.com\",\"subject\":\"Order receipt\",\"identifiers\":{\"email\":\"cool.person@example.com\"},\"message_data\":{\"password_reset_token\":\"abcde-12345-fghij-d888\",\"account_id\":\"123dj\"},\"attachments\":{\"file1.csv\":\"base64encodedcontent\",\"file2.pdf\":\"base64encodedcontent\"},\"headers\":{\"X-Mailgun-Tag\":\"my-cool-tag\"},\"bcc\":\"bcc@example.com\",\"disable_message_retention\":false,\"send_to_unsubscribed\":true,\"tracked\":true,\"queue_draft\":false,\"disable_css_preprocessing\":true}"

response = http.request(request)
puts response.read_body
```

```php
// No official Customer.io SDK for PHP
```

```python
import http.client
import json

conn = http.client.HTTPSConnection("api.customer.io")

payload = json.dumps({
  "transactional_message_id": 44,
  "to": "cool.person@example.com",
  "from": "override-templated-address@example.com",
  "subject": "Order receipt",
  "identifiers": {
    "email": "cool.person@example.com"
  },
  "message_data": {
    "password_reset_token": "abcde-12345-fghij-d888",
    "account_id": "123dj"
  },
  "attachments": {
    "file1.csv": "base64encodedcontent",
    "file2.pdf": "base64encodedcontent"
  },
  "headers": {
    "X-Mailgun-Tag": "my-cool-tag"
  },
  "bcc": "bcc@example.com",
  "disable_message_retention": False,
  "send_to_unsubscribed": True,
  "tracked": True,
  "queue_draft": False,
  "disable_css_preprocessing": True
})
headers = {
  "X-Workspace-Id": "100",
  "Content-Type": "application/json",
  "Authorization": "Bearer YOUR_SECRET_TOKEN"
}

conn.request(
    "POST",
    "/v1/send/email",
    body=payload,
    headers=headers,
)

response = conn.getresponse()
print(response.read().decode())

conn.close()
```

```go
package main

import (
	"fmt"
	"io"
	"net/http"
	"strings"
)

func main() {
	requestUrl := "https://api.customer.io/v1/send/email"

	payload := strings.NewReader(`{
  "transactional_message_id": 44,
  "to": "cool.person@example.com",
  "from": "override-templated-address@example.com",
  "subject": "Order receipt",
  "identifiers": {
    "email": "cool.person@example.com"
  },
  "message_data": {
    "password_reset_token": "abcde-12345-fghij-d888",
    "account_id": "123dj"
  },
  "attachments": {
    "file1.csv": "base64encodedcontent",
    "file2.pdf": "base64encodedcontent"
  },
  "headers": {
    "X-Mailgun-Tag": "my-cool-tag"
  },
  "bcc": "bcc@example.com",
  "disable_message_retention": false,
  "send_to_unsubscribed": true,
  "tracked": true,
  "queue_draft": false,
  "disable_css_preprocessing": true
}`)

	req, _ := http.NewRequest("POST", requestUrl, payload)

	req.Header.Add("X-Workspace-Id", "100")
	req.Header.Add("Content-Type", "application/json")
	req.Header.Add("Authorization", "Bearer YOUR_SECRET_TOKEN")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

```rust
// No official Customer.io SDK for Rust
```

```java
// No official Customer.io SDK for Java
```

```dotnet
// No official Customer.io SDK for .NET
```

```curl
curl https://api.customer.io/v1/send/email \
  --request POST \
  --header 'X-Workspace-Id: 100' \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer YOUR_SECRET_TOKEN' \
  --data '{
  "transactional_message_id": 44,
  "to": "cool.person@example.com",
  "from": "override-templated-address@example.com",
  "subject": "Order receipt",
  "identifiers": {
    "email": "cool.person@example.com"
  },
  "message_data": {
    "password_reset_token": "abcde-12345-fghij-d888",
    "account_id": "123dj"
  },
  "attachments": {
    "file1.csv": "base64encodedcontent",
    "file2.pdf": "base64encodedcontent"
  },
  "headers": {
    "X-Mailgun-Tag": "my-cool-tag"
  },
  "bcc": "bcc@example.com",
  "disable_message_retention": false,
  "send_to_unsubscribed": true,
  "tracked": true,
  "queue_draft": false,
  "disable_css_preprocessing": true
}'
```
</CodeTabs>

_Resend_

<CodeTabs>
```nodejs
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>',
});
```

```ruby
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>"
}

sent = Resend::Emails.send(params)
puts sent
```

```php
$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>'
]);
```

```python
import resend

resend.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>"
}

email = resend.Emails.send(params)
print(email)
```

```go
import (
	"fmt"

	"github.com/resend/resend-go/v2"
)

func main() {
  ctx := context.TODO()
  client := resend.NewClient("re_xxxxxxxxx")

  params := &resend.SendEmailRequest{
      From:        "Acme <onboarding@resend.dev>",
      To:          []string{"delivered@resend.dev"},
      Subject:     "hello world",
      Html:        "<p>it works!</p>"
  }

  sent, err := client.Emails.SendWithContext(ctx, params)

  if err != nil {
    panic(err)
  }
  fmt.Println(sent.Id)
}
```

```rust
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 html = "<p>it works!</p>";

  let email = CreateEmailBaseOptions::new(from, to, subject)
    .with_html(html);

  let _email = resend.emails.send(email).await?;

  Ok(())
}
```

```java
import com.resend.*;

public class Main {
    public static void main(String[] args) {
        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();

        CreateEmailResponse data = resend.emails().send(params);
    }
}
```

```dotnet
using Resend;

IResend resend = ResendClient.Create( "re_xxxxxxxxx" );

var resp = await resend.EmailSendAsync( 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
curl -X POST 'https://api.resend.com/emails' \\
     -H 'Authorization: Bearer re_xxxxxxxxx' \\
     -H 'Content-Type: application/json' \\
     -d $'{
    "from": "Acme <onboarding@resend.dev>",
    "to": ["delivered@resend.dev"],
    "subject": "hello world",
    "html": "<p>it works!</p>"
}'
```
</CodeTabs>

**Events**

Both Customer.io and Resend provide automated workflow journey builders. These are called Campaigns in Customer.io and Automations in Resend. These flows can be triggered by API-driven Events in both products. Events are meant to represent customer actions.

_Customer.io Events_

<CodeTabs>
```nodejs
const { TrackClient, RegionUS } = require('customerio-node');
let cio = new TrackClient(siteId, apiKey, { region: RegionUS });

// Depending on your workspace settings, customer_id may be an email address.
cio.track(5, {
  name: 'purchase',
  data: {
    price: '23.45',
    product: 'socks'
  }
});
```

```ruby
$customerio = Customerio::Client.new("YOUR SITE ID", "YOUR API SECRET KEY", region: Customerio::Regions::US)

// Depending on your workspace settings, customer_id may be an email address.
$customerio.track(5, "purchase", :type => "socks", :price => "13.99", :timestamp => 1365436200)
```

```php
// No official Customer.io SDK for PHP
```

```python
from customerio import CustomerIO, Regions
cio = CustomerIO(site_id, api_key, region=Regions.US)

// Depending on your workspace settings, customer_id may be an email address.
cio.track(customer_id="5", name='purchased', price=23.45, product="widget")
```

```go
track := customerio.NewTrackClient("YOUR SITE ID", "YOUR API SECRET KEY", customerio.WithRegion(customerio.RegionUS))

if err := track.Track("5", "purchase", map[string]interface{}{
    "type": "socks",
    "price": "13.99",
}); err != nil {
    // do something with error
}
```

```rust
// No official Customer.io SDK for Rust
```

```java
// No official Customer.io SDK for Java
```

```dotnet
// No official Customer.io SDK for .NET
```

```curl
curl https://track.customer.io/api/v1/customers/12345/events \
  --request POST \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Basic username:password' \
  --data '{
  "name": "purchase",
  "data": {
    "price": 23.45,
    "product": "socks"
  }
}'
```
</CodeTabs>

_Resend Events_

<CodeTabs>
```nodejs
import { Resend } from 'resend';

const resend = new Resend('re_xxxxxxxxx');

// Trigger with a contact ID
const { data, error } = await resend.events.send({
  event: 'user.created',
  contactId: '7f2e4a3b-dfbc-4e9a-8b2c-5f3a1d6e7c8b',
  payload: {
    plan: 'pro',
  },
});

// Trigger with an email address
const { data, error } = await resend.events.send({
  event: 'user.created',
  email: 'steve.wozniak@gmail.com',
  payload: {
    plan: 'pro',
  },
});
```

```ruby
require "resend"

Resend.api_key = "re_xxxxxxxxx"

# Trigger with a contact ID
params = {
  event: "user.created",
  contact_id: "7f2e4a3b-dfbc-4e9a-8b2c-5f3a1d6e7c8b",
  payload: {
    plan: "pro",
  },
}

Resend::Events.send(params)

# Trigger with an email address
params = {
  event: "user.created",
  email: "steve.wozniak@gmail.com",
  payload: {
    plan: "pro",
  },
}

Resend::Events.send(params)
```

```php
$resend = Resend::client('re_xxxxxxxxx');

// Trigger with a contact ID
$resend->events->send([
  'event' => 'user.created',
  'contact_id' => '7f2e4a3b-dfbc-4e9a-8b2c-5f3a1d6e7c8b',
  'payload' => ['plan' => 'pro'],
]);

// Trigger with an email address
$resend->events->send([
  'event' => 'user.created',
  'email' => 'steve.wozniak@gmail.com',
  'payload' => ['plan' => 'pro'],
]);
```

```python
import resend

resend.api_key = "re_xxxxxxxxx"

# Trigger with a contact ID
params: resend.Events.SendParams = {
  "event": "user.created",
  "contact_id": "7f2e4a3b-dfbc-4e9a-8b2c-5f3a1d6e7c8b",
  "payload": {
    "plan": "pro",
  },
}

resend.Events.send(params)

# Trigger with an email address
params: resend.Events.SendParams = {
  "event": "user.created",
  "email": "steve.wozniak@gmail.com",
  "payload": {
    "plan": "pro",
  },
}

resend.Events.send(params)
```

```go
package main

import "github.com/resend/resend-go/v3"

func main() {
	client := resend.NewClient("re_xxxxxxxxx")

	// Trigger with a contact ID
	params := &resend.SendEventRequest{
		Event:     "user.created",
		ContactId: "7f2e4a3b-dfbc-4e9a-8b2c-5f3a1d6e7c8b",
		Payload: map[string]any{
			"plan": "pro",
		},
	}

	client.Events.Send(params)

	// Trigger with an email address
	params = &resend.SendEventRequest{
		Event: "user.created",
		Email: "steve.wozniak@gmail.com",
		Payload: map[string]any{
			"plan": "pro",
		},
	}

	client.Events.Send(params)
}
```

```rust
use resend_rs::{
  json,
  types::{ContactIdOrEmail, SendEventOptions},
  Resend, Result,
};

#[tokio::main]
async fn main() -> Result<()> {
  let resend = Resend::new("re_xxxxxxxxx");

  let opts = SendEventOptions {
    event: "user.created".to_owned(),
    contact_id_or_email: ContactIdOrEmail::ContactId(
      "7f2e4a3b-dfbc-4e9a-8b2c-5f3a1d6e7c8b".to_owned(),
    ),
    payload: json!({
      "plan": "pro"
    }),
  };

  let opts = SendEventOptions {
    event: "user.created".to_owned(),
    contact_id_or_email: ContactIdOrEmail::Email("steve.wozniak@gmail.com".to_owned()),
    payload: json!({
      "plan": "pro"
    }),
  };

  let _event = resend.events.send(opts).await?;

  Ok(())
}
```

```java
import com.resend.*;

public class Main {
    public static void main(String[] args) {
        Resend resend = new Resend("re_xxxxxxxxx");

        // Trigger with a contact ID
        SendEventOptions params = SendEventOptions.builder()
                .event("user.created")
                .contactId("7f2e4a3b-dfbc-4e9a-8b2c-5f3a1d6e7c8b")
                .addPayload("plan", "pro")
                .build();

        SendEventResponseSuccess data = resend.events().send(params);

        // Trigger with an email address
        SendEventOptions params = SendEventOptions.builder()
                .event("user.created")
                .email("steve.wozniak@gmail.com")
                .addPayload("plan", "pro")
                .build();

        SendEventResponseSuccess data = resend.events().send(params);
    }
}
```

```dotnet
using Resend;
using System.Text.Json;

IResend resend = ResendClient.Create( "re_xxxxxxxxx" );

var payload = JsonSerializer.SerializeToElement( new { plan = "pro" } );

// Trigger with a contact ID
var resp = await resend.EventSendAsync( new EventSendData()
{
    Event = "user.created",
    ContactId = new Guid( "7f2e4a3b-dfbc-4e9a-8b2c-5f3a1d6e7c8b" ),
    Payload = payload,
} );
Console.WriteLine( "Event={0}", resp.Content.Event );

// Trigger with an email address
await resend.EventSendAsync( new EventSendData()
{
    Event = "user.created",
    Email = "steve.wozniak@gmail.com",
    Payload = payload,
} );
```

```curl
# Trigger with a contact ID
curl -X POST 'https://api.resend.com/events/send' \
     -H 'Authorization: Bearer re_xxxxxxxxx' \
     -H 'Content-Type: application/json' \
     -d '{
  "event": "user.created",
  "contact_id": "7f2e4a3b-dfbc-4e9a-8b2c-5f3a1d6e7c8b",
  "payload": {
    "plan": "pro"
  }
}'

# Trigger with an email address
curl -X POST 'https://api.resend.com/events/send' \
     -H 'Authorization: Bearer re_xxxxxxxxx' \
     -H 'Content-Type: application/json' \
     -d '{
  "event": "user.created",
  "email": "steve.wozniak@gmail.com",
  "payload": {
    "plan": "pro"
  }
}'
```
</CodeTabs>

**Contacts / People**

Contacts unlock unsubscribe handling and personalization. Note that Customer.io's endpoint for creating and updating People is the same endpoint, while Resend has separate endpoints for creating and updating Contacts.

_Customer.io Create / Update Person_

<CodeTabs>
```nodejs
const { TrackClient, RegionUS } = require('customerio-node');
let cio = new TrackClient(siteId, apiKey, { region: RegionUS });

cio.identify(5, {
  email: 'customer@example.com',
  created_at: 1361205308,
  first_name: 'Bob',
  plan: 'basic'
});
```

```ruby
$customerio = Customerio::Client.new("YOUR SITE ID", "YOUR API SECRET KEY", region: Customerio::Regions::US)

$customerio.identify(
  :id => 5,
  :email => "bob@example.com",
  :created_at => customer.created_at.to_i,
  :first_name => "Bob",
  :plan => "basic"
)

```

```php
// No official Customer.io SDK for PHP
```

```python
from customerio import CustomerIO, Regions
cio = CustomerIO(site_id, api_key, region=Regions.US)

cio.identify(id="5", email='customer@example.com', name='Bob', plan='premium')
```

```go
track := customerio.NewTrackClient("YOUR SITE ID", "YOUR API SECRET KEY", customerio.WithRegion(customerio.RegionUS))

if err := track.Identify("5", map[string]interface{}{
  "email": "bob@example.com",
  "created_at": time.Now().Unix(),
  "first_name": "Bob",
  "plan": "basic",
}); err != nil {
  // do something with error
}
```

```rust
// No official Customer.io SDK for Rust
```

```java
// No official Customer.io SDK for Java
```

```dotnet
// No official Customer.io SDK for .NET
```

```curl
curl https://track.customer.io/api/v1/customers/12345 \
  --request PUT \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Basic username:password' \
  --data '{
  "email": "customer@example.com",
  "created_at": 1361205308,
  "first_name": "Bob",
  "plan": "basic",
  "cio_relationships": {
    "action": "add_relationships",
    "relationships": [
      {
        "identifiers": {
          "object_type_id": "1",
          "object_id": "01H5Q5SZVQDJ71SBME2SMDXS88"
        },
        "relationship_attributes": {
          "role": "admin"
        }
      },
      {
        "identifiers": {
          "object_type_id": "2",
          "object_id": "1171SBME"
        },
        "relationship_attributes": {
          "role": "viewer"
        }
      }
    ]
  },
  "cio_subscription_preferences": {
    "topics": {
      "topic_1": true,
      "topic_2": true,
      "topic_3": false
    }
  }
}'
```
</CodeTabs>

_Resend Create Contact - see documentation for [updating a contact](/docs/api-reference/contacts/update-contact)_

<CodeTabs>
```nodejs
import { Resend } from 'resend';

const resend = new Resend('re_xxxxxxxxx');

const { data, error } = await resend.contacts.create({
  email: 'steve.wozniak@gmail.com',
  firstName: 'Steve',
  lastName: 'Wozniak',
  unsubscribed: false,
});
```

```ruby
require "resend"

Resend.api_key = "re_xxxxxxxxx"

params = {
  "email": "steve.wozniak@gmail.com",
  "first_name": "Steve",
  "last_name": "Wozniak",
  "unsubscribed": false,
}

Resend::Contacts.create(params)
```

```php
$resend = Resend::client('re_xxxxxxxxx');

$resend->contacts->create(
  parameters: [
    'email' => 'steve.wozniak@gmail.com',
    'first_name' => 'Steve',
    'last_name' => 'Wozniak',
    'unsubscribed' => false
  ]
);
```

```python
import resend

resend.api_key = "re_xxxxxxxxx"

params: resend.Contacts.CreateParams = {
  "email": "steve.wozniak@gmail.com",
  "first_name": "Steve",
  "last_name": "Wozniak",
  "unsubscribed": False,
}

resend.Contacts.create(params)
```

```go
package main

import "github.com/resend/resend-go/v3"

func main() {
	client := resend.NewClient("re_xxxxxxxxx")

	params := &resend.CreateContactRequest{
		Email:        "steve.wozniak@gmail.com",
		FirstName:    "Steve",
		LastName:     "Wozniak",
		Unsubscribed: false,
	}

	client.Contacts.Create(params)
}
```

```rust
use resend_rs::{types::CreateContactOptions, Resend, Result};

#[tokio::main]
async fn main() -> Result<()> {
  let resend = Resend::new("re_xxxxxxxxx");

  let contact = CreateContactOptions::new("steve.wozniak@gmail.com")
    .with_first_name("Steve")
    .with_last_name("Wozniak")
    .with_unsubscribed(false);

  let _contact = resend.contacts.create(contact).await?;

  Ok(())
}
```

```java
import com.resend.*;

public class Main {
    public static void main(String[] args) {
        Resend resend = new Resend("re_xxxxxxxxx");

        CreateContactOptions params = CreateContactOptions.builder()
                .email("steve.wozniak@gmail.com")
                .firstName("Steve")
                .lastName("Wozniak")
                .unsubscribed(false)
                .build();

        CreateContactResponseSuccess data = resend.contacts().create(params);
    }
}
```

```dotnet
using Resend;

IResend resend = ResendClient.Create( "re_xxxxxxxxx" ); // Or from DI

var resp = await resend.ContactAddAsync(
    new ContactData()
    {
        Email = "steve.wozniak@gmail.com",
        FirstName = "Steve",
        LastName = "Wozniak",
        IsUnsubscribed = false,
    }
);
Console.WriteLine( "Contact Id={0}", resp.Content );
```
</CodeTabs>


## Send email via SMTP

Customer.io does not support sending emails via SMTP, but Resend does.

If you prefer, you can continue using Customer.io, but [transition to using Resend's SMTP service for handling email delivery](/docs/send-with-customer-io-smtp).

## Webhooks

Both Customer.io and Resend provide webhooks to notify your application of email events.

**Key differences**

- Customer.io does not support inbound email, so inbound webhooks do not exist in Customer.io. [Resend supports receiving emails](/docs/dashboard/receiving/introduction) and powers the experience with webhooks.

<Table
  data={{
    headers: ["Event", "Customer.io", "Resend"],
    rows: [
      [
        "Inbound",
        "-",
        {
          href: "/docs/dashboard/receiving/introduction",
          text: "Inbound"
        }
      ],
      [
        "Send",
        {
          href: "https://docs.customer.io/integrations/data-out/connections/webhooks/#email-event-examples",
          text: "email_sent"
        },
        {
          href: "/docs/dashboard/webhooks/event-types#email-sent",
          text: "email.sent"
        }
      ],
      [
        "Dropped",
        {
          href: "https://docs.customer.io/integrations/data-out/connections/webhooks/#email-event-examples",
          text: "email_dropped"
        },
        "-"
      ],
      [
        "Delivery",
        {
          href: "https://docs.customer.io/integrations/data-out/connections/webhooks/#email-event-examples",
          text: "email_delivered"
        },
        {
          href: "/docs/dashboard/webhooks/event-types#email-delivered",
          text: "email.delivered"
        }
      ],
      [
        "Delivery Delayed",
        "-",
        {
          href: "/docs/dashboard/webhooks/event-types#email-delivery-delayed",
          text: "email.delivery_delayed"
        }
      ],
      [
        "Bounces",
        {
          href: "https://docs.customer.io/integrations/data-out/connections/webhooks/#email-event-examples",
          text: "email_bounced"
        },
        {
          href: "/docs/dashboard/webhooks/event-types#email-bounced",
          text: "email.bounced"
        }
      ],
      [
        "Complaints",
        {
          href: "https://docs.customer.io/integrations/data-out/connections/webhooks/#email-event-examples",
          text: "email_spammed"
        },
        {
          href: "/docs/dashboard/webhooks/event-types#email-complained",
          text: "email.complained"
        }
      ],
      [
        "Open Tracking",
        {
          href: "https://docs.customer.io/integrations/data-out/connections/webhooks/#email-event-examples",
          text: "email_opened"
        },
        {
          href: "/docs/dashboard/webhooks/event-types#email-opened",
          text: "email.opened"
        }
      ],
      [
        "Click Tracking",
        {
          href: "https://docs.customer.io/integrations/data-out/connections/webhooks/#email-event-examples",
          text: "email_clicked"
        },
        {
          href: "/docs/dashboard/webhooks/event-types#email-clicked",
          text: "email.clicked"
        }
      ],
      [
        "Conversion Tracking",
        {
          href: "https://docs.customer.io/integrations/data-out/connections/webhooks/#email-event-examples",
          text: "email_converted"
        },
        "-",
      ],
      [
        "Unsubscribe",
        {
          href: "https://docs.customer.io/integrations/data-out/connections/webhooks/#email-event-examples",
          text: "email_unsubscribed"
        },
        {
          href: "/docs/dashboard/webhooks/event-types#contact-updated",
          text: "contact.updated"
        }
      ],
      [
        "Failed to Send",
        {
          href: "https://docs.customer.io/integrations/data-out/connections/webhooks/#email-event-examples",
          text: "email_failed"
        },
        {
          href: "/docs/webhooks/emails/failed",
          text: "email.failed"
        }
      ]
    ]
  }}
/>

## Security & Privacy

Customer.io and Resend have robust and similar security features.

<Table
  data={{
    headers: ["Name", "Customer.io", "Resend"],
    rows: [
      ["Authentication", "Email/Password, Google", "Email/Password, Google, GitHub"],
      ["Multi-Factor Auth", "MFA available", "MFA available"],
      [  "GDPR",
        {
          href: "https://www.twilio.com/docs/customer-io/glossary/gdpr",
          text: "GDPR compliant"
        },
        {
          href: "/security/gdpr",
          text: "GDPR compliant"
        }
      ],
      [
        "SOC 2",
        {
          href: "https://customer-io.com/en-us/policies/security",
          text: "SOC 2 compliant"
        },
        {
          href: "/security/soc-2",
          text: "SOC 2 compliant"
        }
      ],
      [
        "HIPAA",
        {
          href: "https://docs.customer.io/journeys/channels/sms/registration/hipaa-standards/",
          text: "HIPAA compliant"
        },
        "-"
      ]
    ]
  }}
/>

## Idempotency Keys

Resend supports idempotency keys on the `POST /emails` and `POST /emails/batch` endpoints.

> Customer.io does not currently support idempotency keys for their standard API methods, but it is available when using their [webhook data-in source](https://docs.customer.io/journeys/send/workflows/webhooks/action/#idempotency).

An idempotent operation is an action you can perform more than once, with the same input, and it always produces the same outcome and avoids repeating side effects.

By adding an `Idempotency-Key` header, or using the equivalent field on our SDKs, you can tell Resend that this specific email should only be sent once, even if we get more than one request from you about it.

`POST /emails`

<CodeTabs>

```nodejs
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',
  },
);
```

```php
$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',
]);
```

```python
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)
```

```ruby
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" }
)
```

```go
ctx := context.TODO()
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)
}
```

```rust
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(())
}
```

```java
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);
```

```dotnet
using Resend;

IResend resend = ResendClient.Create( "re_xxxxxxxxx" );

var 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
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>"
}'
```

</CodeTabs>

`POST /emails/batch`

<CodeTabs codeHeight={450}>

```nodejs
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',
  },
);
```

```php
$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',
  ]
);
```

```python
import resend
from typing import List

resend.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)
```

```ruby
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" }
)
```

```go
package examples

import (
	"fmt"
	"os"

	"github.com/resend/resend-go/v2"
)

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)
}
```

```rust
use resend_rs::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>"),
  ];

  let _emails = resend.batch.send_with_idempotency_key(emails, "team-quota/123456789").await?;

  Ok(())
}
```

```java
import com.resend.*;

public class Main {
    public static void main(String[] args) {
        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),
            Map.of("idempotency_key", "team-quota/123456789")
        );
    }
}
```

```dotnet
using Resend;

IResend resend = ResendClient.Create( "re_xxxxxxxxx" ); // Or from DI

var 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
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>"
  }
]'
```

</CodeTabs>

Adding idempotency keys not only helps you avoid sending duplicate emails and using up your quota, but also enables building more robust and scalable systems.

<LinkCard
  title="Idempotency Keys"
  description="Learn how to send an idempotent email using Resend."
  url="/docs/dashboard/emails/idempotency-keys"
  image="https://cdn.resend.com/posts/idempotency-keys.jpg"
/>

## Additional Features

Outside of core email sending, Resend offers additional features that Customer.io does not have that may be helpful for you.

**Deliverability Insights**

Improve your chances of landing in the inbox instead of the spam folder with detailed recommendations on each email sent.

<LinkCard
  title="Deliverability Insights"
  description="Improve email deliverability by identifying issues and applying best practices."
  url="/blog/deliverability-insights"
  image="https://cdn.resend.com/posts/deliverability-insights.jpg"
/>

**Multi-Region**

Improve your email deliverability speed by using a region nearest to your users.

<LinkCard
  title="Faster Email Delivery with Multi-Region"
  description="Faster deliverability with reduced latency."
  url="/blog/multi-region"
  image="https://cdn.resend.com/posts/multi-region.jpg"
/>

## Pricing

Customer.io and Resend bill differently. Customer.io bills based on Contact records only. Resend gives you the option to pay based on [Contacts](/pricing?product=marketing&contacts=200000), based on [emails](/pricing?product=transactional&contacts=200000), or a combination.

**Key differences**

- **Customer.io doesn't offer a free tier like Resend**. Customer.io's entry-level plan is $100/month minimum.
- Customer.io requires you to tie a Contact to every single email send. In **Resend**, your first 1000 Contacts are free, and Contacts are optional\*.

<Table
  data={{
    headers: ["Contacts", "Customer.io", "Resend"],
    rows: [
      ["1,000", "$100", "$0"],
      ["5,000", "$100", "$40"],
      ["10,000", "$145", "$80"],
      ["25,000", "$325", "$180"],
      ["50,000", "$505", "$250"],
      ["100,000", "$1000+", "$450"],
      ["150,000", "$1000+", "$650"],
      ["200,000+", "Custom", "Custom"],
    ]
  }}
/>

_\*Contacts are only required when using Resend's [Broadcasts](/docs/dashboard/broadcasts/introduction) and [Automations](/docs/dashboard/automations/introduction). If you plan to send only Transactional emails, [see our email-based pricing](/pricing?product=transactional&contacts=200000)._
