Managing Contacts

This guide shows you how to handle the contact tasks that come up in a real integration: importing your user base, honoring opt-outs, and controlling which channel reaches each contact. It assumes you have an API key and can already send messages.

A contact is a validated phone number record that Sent creates and maintains for every number you message. It stores the normalized number, the channels that can reach it (available_channels), the channel Sent prefers (default_channel), and the opt-out flag (opt_out). Sending to a phone number creates or updates its contact automatically and routes the message over the best channel, so you manage contacts directly only for the tasks below. For the routing architecture behind this, see Contacts concepts.

Import your user base

Store each user's phone number in your own database. The phone number (not the Sent contact ID) is what you pass to the messages API; Sent keys the contact record to the number and keeps it updated:

// Store the E.164 phone number on your user record
const user = await db.users.create({
  email: 'jane@example.com',
  phoneNumber: '+1234567890'
});

// Send directly to the phone number — Sent creates or
// updates the contact automatically
await client.messages.send({
  to: [user.phoneNumber],
  template: { id: '7ba7b820-9dad-11d1-80b4-00c04fd430c8' }
});

If your users first hear from you in a campaign, this automatic creation is all you need: the first message to a new number creates its contact. For importing thousands of contacts at once, see the Batch Operations guide.

Pre-create contacts

To run validation and channel detection before your first campaign, create each contact explicitly:

curl -X POST "https://api.sent.dm/v3/contacts" \
  -H "x-api-key: $SENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "phone_number": "+1234567890"
  }'
const contact = await client.contacts.create({
  phoneNumber: '+1234567890'
});

console.log(`Contact created: ${contact.data.id}`);
contact = client.contacts.create(
    phone_number="+1234567890"
)

print(f"Contact created: {contact.data.id}")
contact, err := client.Contacts.Create(context.Background(), sentdm.ContactCreateParams{
    PhoneNumber: sentdm.String("+1234567890"),
})

fmt.Println("Contact created:", contact.Data.ID)
ContactCreateParams params = ContactCreateParams.builder()
    .phoneNumber("+1234567890")
    .build();

var contact = client.contacts().create(params);
System.out.println("Contact created: " + contact.data().id());
ContactCreateParams parameters = new()
{
    PhoneNumber = "+1234567890"
};

var contact = await client.Contacts.Create(parameters);
Console.WriteLine($"Contact created: {contact.Data.Id}");
$contact = $client->contacts->create(
    phoneNumber: '+1234567890'
);

echo "Contact created: " . $contact->data->id . "\n";
contact = sent_dm.contacts.create(
  phone_number: "+1234567890"
)

puts "Contact created: #{contact.data.id}"

If the number already has a contact, the API returns 409 with error code RESOURCE_007. Treat it as already imported and continue.

Handle invalid numbers

Numbers Sent cannot parse are rejected with 400 and error code VALIDATION_002. Surface the error to the user instead of retrying:

try {
  const contact = await client.contacts.create({
    phoneNumber: 'invalid-number'
  });
} catch (error) {
  if (error.code === 'VALIDATION_002') {
    // Prompt user to correct their number
    showValidationError('Please enter a valid phone number');
  }
}

Always include the country code. Sent normalizes any parseable format to E.164:

Input FormatNormalized (E.164)Display Format
+1 234-567-8900+12345678900+1 234-567-8900
(234) 567-8900+12345678900+1 234-567-8900
234-567-8900+12345678900+1 234-567-8900

Numbers without country codes are rejected or may be misrouted.

Verify the import

Filter the contact list by phone number (URL-encode + as %2B) and confirm the record exists with its detected channels:

curl "https://api.sent.dm/v3/contacts?page=1&page_size=100&phone=%2B1234567890" \
  -H "x-api-key: $SENT_API_KEY"

Honor opt-outs

The contact's opt_out flag is the consent record Sent checks on every send. It is per contact, not per channel: setting it suppresses SMS, WhatsApp, and RCS alike. Sent sets the flag automatically when a contact texts an opt-out keyword such as STOP. Set it yourself when a user revokes consent on any other surface (a preference center, a support ticket, an email unsubscribe):

curl -X PATCH "https://api.sent.dm/v3/contacts/6ba7b810-9dad-11d1-80b4-00c04fd430c8" \
  -H "x-api-key: $SENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"opt_out": true}'

Send {"opt_out": false} to restore consent, and only when you hold the contact's documented consent. The API does not check how the opt-out was created.

You do not need a pre-send check of your own: a send to an opted-out contact is still accepted with 202, then finalized as FILTERED, and a message.filtered webhook fires. For the full workflow (mirroring keyword opt-outs into your database and detecting consent-blocked sends), see Handling Opt-Outs and Consent.

Control channel routing

By default, Sent picks each recipient's channel automatically. Two contact fields drive the decision:

  • available_channels: the channels that can reach the number, detected by Sent (for example "sms,whatsapp").
  • default_channel: the channel Sent prefers for this contact. Sent adjusts it based on delivery results, and you can set it yourself.

If a contact should receive messages on a specific channel by default, update default_channel (accepts sms, whatsapp, or rcs):

curl -X PATCH "https://api.sent.dm/v3/contacts/6ba7b810-9dad-11d1-80b4-00c04fd430c8" \
  -H "x-api-key: $SENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "default_channel": "whatsapp"
  }'
const updated = await client.contacts.update('6ba7b810-9dad-11d1-80b4-00c04fd430c8', {
  defaultChannel: 'whatsapp'
});
updated = client.contacts.update(
    "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
    default_channel="whatsapp"
)
updated, err := client.Contacts.Update(context.Background(), "6ba7b810-9dad-11d1-80b4-00c04fd430c8", sentdm.ContactUpdateParams{
    DefaultChannel: sentdm.String("whatsapp"),
})
ContactUpdateParams params = ContactUpdateParams.builder()
    .defaultChannel("whatsapp")
    .build();

var updated = client.contacts().update("6ba7b810-9dad-11d1-80b4-00c04fd430c8", params);
ContactUpdateParams parameters = new()
{
    DefaultChannel = "whatsapp"
};

var updated = await client.Contacts.Update("6ba7b810-9dad-11d1-80b4-00c04fd430c8", parameters);
$updated = $client->contacts->update(
    "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
    defaultChannel: 'whatsapp'
);
updated = sent_dm.contacts.update(
  "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
  default_channel: "whatsapp"
)

Before switching a contact's default, confirm the target channel can reach them:

const contact = await client.contacts.get(contactId);

// Check available channels before switching the default
if (!contact.data.availableChannels.includes('whatsapp')) {
  console.log('WhatsApp not available for this contact — keep SMS');
}

The update response echoes the contact, so default_channel in the response confirms the change took effect.

To pin a single send to a channel regardless of the contact's default, or to broadcast on several channels at once, set the channel field on the message itself. Refer to Channel Selection Strategies for how pinning disables cross-channel fallback.

Find and inspect contacts

Look up contacts when you reconcile your database with Sent or debug routing. GET /v3/contacts requires page and page_size, and supports search, phone, and channel filters:

# List with pagination
curl "https://api.sent.dm/v3/contacts?page=1&page_size=100" \
  -H "x-api-key: $SENT_API_KEY"

# Search contacts (URL-encode + as %2B)
curl "https://api.sent.dm/v3/contacts?page=1&page_size=100&search=%2B1234" \
  -H "x-api-key: $SENT_API_KEY"
// List all contacts
const contacts = await client.contacts.list({
  page: 1,
  page_size: 100
});

// Search contacts
const searchResults = await client.contacts.list({
  search: '+1234'
});

// Iterate through all pages
for (const contact of contacts.data) {
  console.log(`${contact.phoneNumber} - ${contact.availableChannels}`);
}
# List all contacts
contacts = client.contacts.list(page=1, page_size=100)

# Search contacts
search_results = client.contacts.list(search="+1234")

# Iterate through results
for contact in contacts.data:
    print(f"{contact.phone_number} - {contact.available_channels}")
// List all contacts
contacts, err := client.Contacts.List(context.Background(), sentdm.ContactListParams{
    Page:     sentdm.Int(1),
    PageSize: sentdm.Int(100),
})

// Search contacts
searchResults, err := client.Contacts.List(context.Background(), sentdm.ContactListParams{
    Search: sentdm.String("+1234"),
})

// Iterate through results
for _, contact := range contacts.Data {
    fmt.Printf("%s - %s\n", contact.PhoneNumber, contact.AvailableChannels)
}
// List all contacts
ContactListParams params = ContactListParams.builder()
    .page(1)
    .pageSize(100)
    .build();

var contacts = client.contacts().list(params);

// Search contacts
var searchResults = client.contacts().list(ContactListParams.builder()
    .search("+1234")
    .build());

// Iterate through results
for (var contact : contacts.data()) {
    System.out.println(contact.phoneNumber() + " - " + contact.availableChannels());
}
// List all contacts
ContactListParams parameters = new()
{
    Page = 1,
    PageSize = 100
};

var contacts = await client.Contacts.List(parameters);

// Search contacts
var searchResults = await client.Contacts.List(new ContactListParams
{
    Search = "+1234"
});

// Iterate through results
foreach (var contact in contacts.Data)
{
    Console.WriteLine($"{contact.PhoneNumber} - {contact.AvailableChannels}");
}
// List all contacts
$contacts = $client->contacts->list(page: 1, pageSize: 100);

// Search contacts
$searchResults = $client->contacts->list(search: '+1234');

// Iterate through results
foreach ($contacts->data as $contact) {
    echo "{$contact->phone_number} - {$contact->available_channels}\n";
}
# List all contacts
contacts = sent_dm.contacts.list(page: 1, page_size: 100)

# Search contacts
search_results = sent_dm.contacts.list(search: "+1234")

# Iterate through results
contacts.data.each do |contact|
  puts "#{contact.phone_number} - #{contact.available_channels}"
end

Each contact carries the fields the tasks in this guide depend on:

{
  "success": true,
  "data": {
    "id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
    "phone_number": "+1234567890",
    "format_e164": "+1234567890",
    "format_international": "+1 234-567-890",
    "format_national": "(234) 567-890",
    "format_rfc": "tel:+1-234-567-890",
    "country_code": "1",
    "region_code": "US",
    "available_channels": "sms,whatsapp",
    "default_channel": "whatsapp",
    "opt_out": false,
    "is_inherited": false,
    "created_at": "2025-01-01T12:00:00Z",
    "updated_at": "2025-01-15T08:30:00Z"
  },
  "error": null,
  "meta": {
    "request_id": "req_contact_001",
    "timestamp": "2026-03-04T11:28:25.2096416+00:00",
    "version": "v3"
  }
}

To fetch one contact by ID, call GET /v3/contacts/{id}. Refer to the get contact reference for the response contract.

Delete a contact

If a user asks you to remove their data, delete the contact:

curl -X DELETE "https://api.sent.dm/v3/contacts/6ba7b810-9dad-11d1-80b4-00c04fd430c8" \
  -H "x-api-key: $SENT_API_KEY"

Deleting a contact is permanent. Messages already sent to this contact retain their delivery records, but you can no longer reference the contact in future API calls.

Contacts API reference

This guide covers only what these tasks need. For the full contract of every endpoint (parameters, response fields, and error codes), refer to the Contacts API reference:

Next steps

On this page