# 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](/docs/guides/whatsapp/message-types/interactive/contact-info-requests) 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:

```json
{
  "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"
}
```

| `origin`          | How the card arrived                                         |
| ----------------- | ------------------------------------------------------------ |
| `contact_request` | The contact tapped a button you sent asking for their number |
| `other`           | The 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.

| Field                                                    | On a button tap                                   | On a card shared in the chat     |
| -------------------------------------------------------- | ------------------------------------------------- | -------------------------------- |
| `phone_numbers[].phone_number`, `type`                   | The number the contact chose to disclose          | Whatever numbers the card holds  |
| `vcard`                                                  | Omitted; a tap carries the number alone           | The card in vCard format         |
| `name`, `org`, `birthday`, `emails`, `urls`, `addresses` | Whatever WhatsApp sends, which is usually nothing | Present when the card holds them |

```json
{
  "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](/docs/guides/whatsapp/business-scoped-user-ids) 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:

```json
{
  "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

- [How receiving works](/docs/guides/whatsapp/receiving-whatsapp): the inbound envelope, media fetching, and the `whatsapp.received` webhook
- [WhatsApp contact cards](/docs/guides/whatsapp/message-types/contact-cards): the send side of the same arm
- [Business-scoped user IDs](/docs/guides/whatsapp/business-scoped-user-ids): why a contact arrives without a phone number, and how the ask fits the conversation
- [WhatsApp contact info requests](/docs/guides/whatsapp/message-types/interactive/contact-info-requests): the button that asks for a number