WhatsApp contact cards
A contact card message shares one or more contacts: a name the recipient sees on the card, and a profile view they open from it holding phone numbers, emails, websites, addresses, an employer, and a birthday. Use it to hand a customer a colleague's number, a courier's, or your own, instead of pasting digits into text they then have to retype.
Send a contact card
contact_cards is an array. Each card needs a name, and that name needs formatted_name plus at least one other part:
const msg = await bird.whatsapp.send({
to: "+16505551234",
from: "+13124495648",
contact_cards: [
{
name: {
formatted_name: "Barbara J. Johnson",
first_name: "Barbara",
last_name: "Johnson",
},
phone_numbers: [{ phone_number: "+16505559999", type: "Mobile" }],
},
],
});
console.log(msg.id, msg.status);msg = client.whatsapp.send(
to="+16505551234",
from_="+13124495648",
contact_cards=[
{
"name": {
"formatted_name": "Barbara J. Johnson",
"first_name": "Barbara",
"last_name": "Johnson",
},
"phone_numbers": [{"phone_number": "+16505559999", "type": "Mobile"}],
}
],
)
print(msg.id, msg.status)package main
import (
"context"
"fmt"
"log"
"os"
bird "github.com/messagebird/bird-sdk-go"
"github.com/messagebird/bird-sdk-go/option"
)
func main() {
client, err := bird.NewClient(option.WithAPIKey(os.Getenv("BIRD_API_KEY")))
if err != nil {
log.Fatal(err)
}
msg, err := client.Whatsapp.Send(context.Background(), bird.WhatsappSendParams{
To: "+16505551234",
From: "+13124495648",
ContactCards: []bird.WhatsAppContactCardSend{{
Name: bird.WhatsAppContactNameSend{
FormattedName: "Barbara J. Johnson",
FirstName: bird.String("Barbara"),
LastName: bird.String("Johnson"),
},
PhoneNumbers: &[]bird.WhatsAppContactPhoneSend{{
PhoneNumber: "+16505559999",
Type: bird.String("Mobile"),
}},
}},
})
if err != nil {
log.Fatal(err)
}
fmt.Println(msg.Id, *msg.Status)
}$name = (new WhatsAppContactCardSendName())
->setFormattedName('Barbara J. Johnson')
->setFirstName('Barbara')
->setLastName('Johnson');
$phone = (new WhatsAppContactPhoneSend())
->setPhoneNumber('+16505559999')
->setType('Mobile');
$card = (new WhatsAppContactCardSend())
->setName($name)
->setPhoneNumbers([$phone]);
$message = $bird->whatsapp->send(
to: '+16505551234',
from: '+13124495648',
contactCards: [$card],
);
echo $message->getId(), ' ', $message->getStatus();bird whatsapp send \
--to +16505551234 \
--from +13124495648 \
--contact-cards '[{"name":{"formatted_name":"Barbara J. Johnson","first_name":"Barbara","last_name":"Johnson"},"phone_numbers":[{"phone_number":"+16505559999","type":"Mobile"}]}]'curl -X POST "https://us1.platform.bird.com/v1/whatsapp/messages" \
-H "Authorization: Bearer $BIRD_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+16505551234",
"from": "+13124495648",
"contact_cards": [
{
"name": {
"formatted_name": "Barbara J. Johnson",
"first_name": "Barbara",
"last_name": "Johnson"
},
"phone_numbers": [
{ "phone_number": "+16505559999", "type": "Mobile" }
]
}
]
}'from is required on every service message: a number your workspace owns, not a Bird-managed one.
The full shape adds an employer, a birthday, and the other contact-detail arrays:
Exemplo de código
{
"to": "+16505551234",
"from": "+13124495648",
"contact_cards": [
{
"name": {
"formatted_name": "Dr. Barbara J. Johnson Esq.",
"prefix": "Dr.",
"first_name": "Barbara",
"middle_name": "Joana",
"last_name": "Johnson",
"suffix": "Esq."
},
"org": { "company": "Lucky Shrub", "department": "Legal", "title": "Lead Counsel" },
"birthday": "1999-01-23",
"phone_numbers": [
{ "phone_number": "+16505559999", "type": "Landline" },
{ "phone_number": "+19175559999", "type": "Mobile" }
],
"emails": [{ "email": "bjohnson@example.com", "type": "Work" }],
"urls": [{ "url": "https://example.com", "type": "Company" }],
"addresses": [
{
"street": "1 Lucky Shrub Way",
"city": "Menlo Park",
"state": "CA",
"zip": "94025",
"country": "United States",
"country_code": "US",
"type": "Office"
}
]
}
]
}Every type label, on a phone, an email, a website, or an address alike, is free text you write, sent exactly as you wrote it, and shown beside the value in the recipient's profile view. WhatsApp defines no vocabulary for these, so Mobile, Landline, Pop-Up and Work (old) are all equally valid.
What earns a card a button
A phone number written in E.164, with its country code and leading +, earns that card a button that opens a WhatsApp chat with the number. A number Bird cannot read as E.164 still renders on the card, exactly as you wrote it; it just earns no button.
That includes a number written without its leading +. Bird will not add one for you: a national-format number from one country can parse as a valid number in another once a + is bolted on, which would point the button at a stranger. Declining to guess costs a button; guessing wrong costs the recipient a chat with the wrong person.
A card carrying no phone number at all renders with no chat button, and can only be saved to an address book.
Limits
| Field | Bound | Enforced by |
|---|---|---|
| contact_cards | 1 to 5 cards per message | Bird, at accept (422) |
| name | required; formatted_name plus one other name part | Bird, at accept (422) |
| formatted_name, first_name, middle_name, last_name | up to 256 characters | Bird, at accept (422) |
| prefix, suffix | up to 64 characters | Bird, at accept (422) |
| birthday | optional, YYYY-MM-DD, and a date the calendar holds | Bird, at accept (422) |
| phone_numbers, emails, urls, addresses | up to 10 entries each | Bird, at accept (422) |
| phone_number | up to 32 characters | Bird, at accept (422) |
| up to 254 characters | Bird, at accept (422) | |
| url | up to 2048 characters, not validated as a URL | Bird, at accept (422) |
| type on any phone, email, website, or address | up to 64 characters of free text | Bird, at accept (422) |
| company, department, title | up to 128 characters | Bird, at accept (422) |
| street, city, state, zip, country, country_code | up to 128 characters | Bird, at accept (422) |
The five-card cap is Bird's, and it is deliberately far below what WhatsApp accepts. WhatsApp's own published API description declares five, its prose recommends fewer for usability and negative-feedback reasons, and a message that opens as "Contact 1 and 256 other contacts" is a spam vector before it is a feature. Raising the cap later would be an additive change, so ask if five is short for what you're building.
Every length limit above is Bird's too. WhatsApp enforces none worth the name and its client does not compensate: a 500-character type renders as ten lines of one repeated letter, and a 4000-character url is dropped silently, leaving the profile view blank. A 422 naming the offending field beats a card the recipient cannot read.
Two rules the schema cannot express
A name needs a second part. formatted_name alone is refused with a 422 E15061 WhatsAppContactNameIncomplete, naming contact_cards.<n>.name. Any one of prefix, first_name, middle_name, last_name, or suffix satisfies it, but a blank or whitespace-only value does not count, and an org does not rescue it. This is WhatsApp's own requirement, documented nowhere in its reference; Bird catches it at accept so you get an actionable error instead of an asynchronous failure.
A birthday has to be a real date. birthday is YYYY-MM-DD; any other shape, and any date the calendar does not hold, such as 2026-02-30, is refused with a 422 E15062 WhatsAppContactBirthdayInvalid. WhatsApp itself accepts 2026-02-30 and shows it to the recipient, which looks like a bug in your data.
Reading a card back
A card you sent reads back on the same contact_cards field an inbound card uses, through the message list or GET /v1/whatsapp/messages/{id}:
Exemplo de código
{
"id": "wam_01kyb2m4xq7whs0d8n3prv6tez",
"direction": "outbound",
"status": "delivered",
"contact_cards": [
{
"name": { "formatted_name": "Barbara J. Johnson", "first_name": "Barbara" },
"phone_numbers": [{ "phone_number": "+16505559999", "type": "Mobile" }]
}
]
}origin and vcard are absent on a card you sent: WhatsApp sets both on a card a contact shared. A type label you sent reads back exactly as written, while a label on a received card is lowercased. See Receiving WhatsApp messages for the inbound side.
Edge cases
- The customer service window has to be open. A contact card send is a service message, deliverable only inside an open window; see the hub's customer service window.
- There is no wa_id to send. WhatsApp identifies a card's contact by an account ID; Bird derives it from each E.164 phone_number rather than accepting one, so the button on a card can never point somewhere other than the digits printed on it.
- vcard is read-only. WhatsApp generates it for a card a contact shared. There is no way to send a card as raw vCard text.
- A card is not a contact record. Sending one shares details in a message; it creates nothing in your workspace, and the recipient saving it is their own action, invisible to you.
Next steps
- WhatsApp service messages: the customer service window and the model every service message shares
- Contact info requests: ask a contact for their number instead of sending one
- Receiving WhatsApp messages: inbound messages, media, and the whatsapp.received webhook
- How sending works: the request envelope, the 202 model, and safe retries