Sign inGet started

Receiving WhatsApp contact cards

contact_cards is the one arm that carries the same field in both directions. A contact can share a card from their address book, and a tap on a contact info request you sent arrives here too, carrying the number they chose to disclose.

What an inbound contact card carries

contact_cards is always an array, and origin says how the card arrived:
Ejemplo de código
{
  "id": "wam_01kyg9v3timy1w7n0r4cbh9ukf",
  "direction": "inbound",
  "from": { "phone_number": "+14155550100" },
  "to": { "phone_number": "+13124495569" },
  "status": "received",
  "contact_cards": [
    {
      "origin": "contact_request",
      "phone_numbers": [{ "phone_number": "+14155550100", "type": "cell" }]
    }
  ],
  "created_at": "2026-08-25T09:27:45Z"
}
originHow the card arrived
contact_requestThe contact tapped a button you sent asking for their number
otherThe contact shared a card in the chat, unprompted
Check origin before treating a card as an answer to your ask. It is the only signal that distinguishes the two, and a card shared unprompted can name a third party entirely rather than the contact. The value list is open, so treat one you do not recognize as another way of sharing added since.
A card this workspace sent reads back with no origin at all, which is how an outbound card is told from an inbound one on the same field.

What a tap carries, and what a shared card carries

The two arrive with different amounts of detail, and nothing on a card is required: WhatsApp sends the parts the card holds and omits the rest, so a card carrying only an origin still arrives rather than being dropped.
FieldOn a button tapOn a card shared in the chat
phone_numbers[].phone_number, typeThe number the contact chose to discloseWhatever numbers the card holds
vcardOmitted; a tap carries the number aloneThe card in vCard format
name, org, birthday, emails, urls, addressesWhatever WhatsApp sends, which is usually nothingPresent when the card holds them
Ejemplo de código
{
  "contact_cards": [
    {
      "origin": "other",
      "vcard": "BEGIN:VCARD\nVERSION:3.0\nN:Johnson;Barbara;;;\nTEL;type=CELL:+16505551234\nEND:VCARD\n",
      "name": {
        "formatted_name": "Barbara J. Johnson",
        "first_name": "Barbara",
        "last_name": "Johnson"
      },
      "org": { "company": "Northside Plumbing" },
      "phone_numbers": [{ "phone_number": "+16505551234", "type": "cell" }]
    }
  ]
}
Two fields need care when you parse them. phone_number is normalized to E.164 where it can be parsed, and passed through exactly as the contact's device stored it where it cannot, an extension among them, so parse defensively rather than assuming E.164. birthday comes off that device unvalidated and is passed through as text in YYYY-MM-DD shape rather than typed as a date, so do not assume it parses. A type label on a received card is lowercased, and WhatsApp defines no vocabulary for it, so match it case-insensitively rather than switching on CELL.

The phone number a contact discloses

A contact who has adopted a WhatsApp username reaches you by business-scoped user ID with no phone number on from. A contact info request is how you ask for the number, and this arm is where the answer arrives, with origin: "contact_request" and the number in phone_numbers.
The number they disclose is not guaranteed to be the number they chat from: Meta warns that a user's identifier and phone number may not always match, so store the disclosed number as its own fact rather than overwriting the identity on from.

The webhook payload

whatsapp.received carries the contact_cards array on the event envelope:
Ejemplo de código
{
  "type": "whatsapp.received",
  "timestamp": "2026-08-25T09:27:45.019Z",
  "data": {
    "whatsapp_id": "wam_01kyg9v3timy1w7n0r4cbh9ukf",
    "workspace_id": "ws_01ky7m235keycbnwyajabe1a6b",
    "direction": "inbound",
    "from": { "phone_number": "+14155550100", "bsuid": "US.13491208655302741918" },
    "to": { "phone_number": "+13124495569" },
    "contact_cards": [
      {
        "origin": "contact_request",
        "phone_numbers": [{ "phone_number": "+14155550100", "type": "cell" }]
      }
    ],
    "tags": null,
    "metadata": null
  }
}

Things to watch

  • A declined request produces nothing. WhatsApp shows the contact a share sheet, and dismissing it sends no message and fires no webhook, so a flow waiting on a number needs its own timeout rather than a decline event to watch for.
  • Two outstanding asks are indistinguishable. A card answering a contact info request carries no in_reply_to_message_id, so a second ask sent before the first is answered cannot be matched to its own answer.
  • The array can hold several cards. A contact sharing multiple cards in one message fills several entries, each with its own origin.
  • A card is contact data you did not collect. It can carry a third party's name, numbers, and birthday, so apply the same retention and consent rules you would to any other personal data before storing it.

Next steps