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 Format | Normalized (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}"
endEach 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
- Sending Messages: template sends, free-form text, and channel selection
- Batch Operations: import contacts in bulk
- Handling Opt-Outs and Consent: mirror opt-outs into your database and detect consent-blocked sends
- Contacts concepts: the architecture behind channel detection and routing
Handling Opt-Outs and Consent
How to mirror Sent opt-outs into your own database, set contact opt-out state through the API, detect consent-blocked sends, and apply a stricter send window.
Working with Templates
Start here for Sent message templates: build one in the dashboard, look up the definition JSON in the reference, and learn how templates and approval work.