Two Similar Requests, Two Different Operations
“Add this person to WhatsApp contacts” and “send this person’s contact card” may sound similar, but they describe two different API operations.
- Add a contact changes the connected WhatsApp account’s address book.
- Send a vCard delivers structured contact information inside a conversation.
Sending a vCard does not save that person to the connected account’s contacts. Adding a contact does not send anything to a user.
Choosing the wrong operation can create confusing workflows, duplicate mutations, and incorrect retry behavior.
Quick Decision Table
| Requirement | API operation | Result |
|---|---|---|
| Save someone to the connected account’s contact list | POST /v1/accounts/{account_id}/contacts/add |
Returns a contact object and identifiers |
| Share a person’s contact details in a chat |
POST /v1/messages with message.type: "contact"
|
Returns an accepted message result |
| Save a customer and send a representative’s card | Call both operations separately | Two independent results and retry paths |
The key distinction is state versus content:
Add contact
-> changes account state
Send vCard
-> sends conversation content
Option 1: Add a Contact to the Connected Account
Use the add-contact operation when your application needs to save someone to the connected WhatsApp account’s contact list.
curl -X POST \
"https://api.unifyport.ai/v1/accounts/acc_example/contacts/add" \
-H "X-Api-Key: $UNIFYPORT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone_number": "15550001111",
"whatsapp_options": {
"first_name": "Jane",
"full_name": "Jane Doe",
"sync_to_device_contacts": false
}
}'
The request must contain at least one of:
phone_numberusername
WhatsApp-specific fields belong inside whatsapp_options:
first_namefull_namesync_to_device_contacts
Do not move these fields to the top level of the request.
A successful response returns the contact representation. Preserve identifiers such as id and conversation_id from the response.
Do not construct a conversation identifier from the phone number. The provider’s canonical identifier may use a different format, so the API response should remain the source of truth.
See the complete Add contact API reference.
Option 2: Send a vCard in a Conversation
Use the unified message endpoint when the requirement is to share contact details with a user or group.
curl -X POST \
"https://api.unifyport.ai/v1/messages" \
-H "X-Api-Key: $UNIFYPORT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"account_id": "acc_example",
"to": {
"id": "15550002222@s.whatsapp.net",
"type": "user"
},
"message": {
"type": "contact",
"contacts": [
{
"name": "Jane Doe",
"phones": [
{
"number": "+15550001111",
"type": "CELL"
}
],
"emails": [
{
"address": "jane@example.com"
}
],
"organization": "Example Company",
"title": "Product Manager"
}
]
}
}'
Every card in message.contacts requires a non-empty name.
The following fields are optional:
phones[].numberphones[].typeemails[].addressorganizationtitle
You can send one or multiple contact cards in the same request.
The API generates the vCard from structured JSON, so you do not need to construct raw BEGIN:VCARD content manually.
See the complete Send contact message API reference.
Why These Calls Should Remain Separate
Consider a customer-support handoff:
- Save the customer to the connected WhatsApp account.
- Send the customer a contact card for the assigned account manager.
Although both steps involve contacts, they have different side effects.
Step 1: Add customer
Result: Address-book mutation
Step 2: Send account manager's card
Result: Outbound message
Store the two results independently.
A successful address-book update does not prove that the contact message was accepted. An accepted contact message does not prove that the address book changed.
This separation becomes especially important during retries.
If sending the vCard fails after the contact was successfully added, retry only the message operation. Repeating the already successful address-book mutation adds unnecessary work and may produce provider-specific conflicts.
Model the Workflow Explicitly
A simple TypeScript workflow could look like this:
type ContactWorkflowResult = {
addedContactId?: string;
conversationId?: string;
sentMessageId?: string;
};
async function addCustomerAndSendRepresentativeCard(
accountId: string,
customerPhone: string,
): Promise<ContactWorkflowResult> {
const result: ContactWorkflowResult = {};
const contact = await addContact({
accountId,
phoneNumber: customerPhone,
whatsappOptions: {
fullName: "Customer",
syncToDeviceContacts: false,
},
});
result.addedContactId = contact.id;
result.conversationId = contact.conversation_id;
const message = await sendContactMessage({
accountId,
to: {
id: contact.conversation_id,
type: "user",
},
contacts: [
{
name: "Jane Doe",
phones: [{ number: "+15550001111", type: "CELL" }],
organization: "Example Company",
title: "Account Manager",
},
],
});
result.sentMessageId = message.message_id;
return result;
}
In a production system, persist the successful result of each step before starting the next one. That allows a failed workflow to resume from the correct operation.
Handle Errors by Operation
Do not place every contact-related error into one generic handler.
| Operation | Error | Meaning |
|---|---|---|
| Add contact | 400 invalid_request |
Neither phone_number nor username is valid or present |
| Add contact | 501 unsupported_by_provider |
The provider does not implement address-book contact creation |
| Send vCard | 400 invalid_request |
The contacts array is empty or a card is missing name
|
| Send vCard | 400 unsupported_message_type |
The selected account does not support structured contact messages |
A practical error-handling structure is:
try {
await executeContactOperation();
} catch (error) {
switch (error.code) {
case "invalid_request":
// Correct the request instead of retrying it unchanged.
break;
case "unsupported_by_provider":
case "unsupported_message_type":
// Use a documented fallback or disable the feature.
break;
default:
// Apply bounded retry rules only to transient failures.
throw error;
}
}
Validation and capability errors should not be retried with the same request.
For transient failures, use bounded retries and an idempotency strategy appropriate to the operation. Never treat a message retry as permission to repeat a completed address-book mutation.
Design a Cross-Provider Fallback
Structured contact messages are currently WhatsApp-specific in this integration.
Before exposing a universal “send contact” button across WhatsApp, Telegram, LINE, TikTok, Zalo, and X, check the provider message support matrix.
When structured cards are unavailable, a text fallback can preserve the essential information:
Jane Doe
Product Manager, Example Company
Phone: +1 555-000-1111
Email: jane@example.com
Make the fallback explicit in your product design. Silently converting structured content can create inconsistent formatting and privacy behavior across channels.
Keep Personal Data Minimal
Contact cards can contain personal information. Only include fields needed for the actual workflow.
Avoid sending phone numbers, email addresses, job titles, or organization details simply because the schema allows them.
Also remember that sending a vCard does not force the recipient to save it. The recipient’s WhatsApp client and their own actions determine how the card is displayed and whether it becomes a saved contact.
Implementation Checklist
Before shipping, verify that:
- The product requirement distinguishes address-book changes from chat messages.
- Add-contact requests contain
phone_numberorusername. - WhatsApp-specific fields are placed under
whatsapp_options. - Returned contact and conversation identifiers are persisted.
- Contact messages contain a non-empty
contactsarray. - Every card contains
name. - Add and send results are recorded independently.
- Retries resume from the failed step.
- Capability errors do not enter an automatic retry loop.
- Unsupported providers have an explicit fallback.
- Only necessary personal data is included.
- Tests confirm that sending a vCard does not trigger contact creation.
Final Takeaway
Use /contacts/add when you need to change the connected WhatsApp account’s contact list.
Use /v1/messages with message.type: "contact" when you need to deliver contact details inside a chat.
Some workflows need both, but they should remain two explicit operations with separate results, errors, and retry policies.
Originally published on UnifyPort.
Disclosure: AI-assisted drafting and editing were used to prepare this DEV adaptation. The technical content was reviewed against the linked API documentation.
Top comments (0)