---
title: "The Contacts facade — Contacts for Laravel | Roundly"
description: "Contacts::for($owner) for an owner’s contact book, verification() for the token flow, flat calls for one contact — plus the builder and ContactData."
url: https://roundly-consulting.com/open-source/docs/contacts-for-laravel/facade
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Contacts for Laravel](https://roundly-consulting.com/open-source/docs/contacts-for-laravel.md)

# The Contacts facade

The Contacts facade is the recommended entry point. Contacts::for($owner) returns the owner’s contact book, Contacts::verification() runs the token and code flow, and single-contact operations are flat. Every write goes through a dedicated action, so normalization, validation, primary handling and events are identical whichever entry point you use:

```php
use RoundlyConsulting\Contacts\DataTransferObjects\ContactData;
use RoundlyConsulting\Contacts\Enums\ContactType;
use RoundlyConsulting\Contacts\Facades\Contacts;

// Add — normalized + validated, primary guaranteed unique per kind.
Contacts::for($user)->email('a@b.com')->label('Work')->primary()->add();
Contacts::for($user)->phone('+421 900 000 000')->label('Mobile')->add();
Contacts::for($user)->type('whatsapp')->value('+421900123456')->add();   // a registered custom kind
Contacts::for($user)->add(new ContactData(ContactType::Url, 'example.com'));

// Read and export.
Contacts::for($user)->all();                        // ordered by position
Contacts::for($user)->ofType(ContactType::Email);
Contacts::for($user)->primary(ContactType::Email);  // ?Contact
Contacts::for($user)->vCard();                      // vCard 3.0 string

// One contact.
Contacts::update($contact, new ContactData(ContactType::Email, 'new@b.com'));
Contacts::setPrimary($contact);
Contacts::delete($contact);

// Verification.
$token = Contacts::verification()->request($contact);
Contacts::verification()->confirm($contact, $token);
Contacts::verification()->markVerified($contact);
```

## Facade methods

| Method | Returns | Description |
| --- | --- | --- |
| `for(Model $owner)` | `ContactBook` | The owner’s contact book — fluent builders, add, sync, reads and vCard export. |
| `verification()` | `ContactVerification` | The verification accessor: request(), confirm(), markVerified(). |
| `update(Contact $contact, ContactData $data)` | `Contact` | Overwrite a contact; a primary flag promotes it, a new value or kind drops the verification, a structured address replaces the contact’s own. Fires ContactUpdated. |
| `delete(Contact $contact)` | `void` | Soft delete; deleting the primary promotes the next one (auto\_primary). Fires ContactDeleted. |
| `setPrimary(Contact $contact)` | `Contact` | Promote, demoting the owner’s other primary of the same kind; fires PrimaryContactChanged. A soft-deleted contact throws PrimaryContactConflict. |
| `sharedWith(Connectable $owner)` | `Collection` | Contacts connected to an owner (see Affiliations). |
| `validationRules(string $key = 'contacts')` | `array` | contacts.\* rules for a repeatable form. |
| `fake()` | `ContactsFake` | Swap in the recording fake (see Testing). |

## The contact book

Contacts::for($owner) — or $owner->contactBook() on a HasContacts model — returns a ContactBook scoped to that owner:

| Method | Returns | Description |
| --- | --- | --- |
| `email() / phone() / url() / address(string $value)` | `PendingContact` | Start a fluent contact of that kind. |
| `structuredAddress(AddressData\|array $data)` | `PendingContact` | Start an address contact backed by a structured address. |
| `type(ContactType\|string $type)` | `PendingContact` | Start a contact of any kind — a ContactType or a registered custom kind. |
| `add(ContactData $data)` | `Contact` | Add a contact from a DTO; fires ContactAdded. |
| `sync(ContactType\|string $type, array $items)` | `EloquentCollection` | Reconcile the owner’s contacts of one kind — a ContactType or a registered custom kind — to a list; a synced-away primary hands over to the first synced contact. |
| `all()` | `EloquentCollection` | Every contact of the owner, by position. |
| `ofType(ContactType\|string $type)` | `EloquentCollection` | One kind, by position. |
| `primary(ContactType\|string $type)` | `?Contact` | The primary contact of a kind, or null. |
| `vCard()` | `string` | A vCard 3.0 string of the owner’s contacts. |

## The verification accessor

Contacts::verification() returns a ContactVerification (see Verification):

| Method | Returns | Description |
| --- | --- | --- |
| `request(Contact $contact)` | `string` | Issue a code or token and return the plaintext; fires ContactVerificationRequested. |
| `confirm(Contact $contact, string $token)` | `Contact` | Confirm the token and mark the contact verified; fires ContactVerified. |
| `markVerified(Contact $contact, ?CarbonInterface $at = null)` | `Contact` | Mark verified without a token (idempotent); fires ContactVerified. |

## The fluent builder

The book’s email(), phone(), url(), address(), structuredAddress() and type() start a PendingContact. Chain the setters, then call add():

```php
$contact = Contacts::for($company)
    ->email('Sales@Example.com')      // or ->phone() / ->url() / ->address()
    ->label('Sales')                  // free-text label for this contact
    ->name('Northwind sales desk')    // display name
    ->category('Suppliers')           // group contacts
    ->meta(['hours' => '9-17'])       // arbitrary JSON metadata
    ->primary()                       // ->primary(false) to opt out
    ->add();                          // terminal: validates, saves, returns Contact

// Explicit type + value
Contacts::for($company)->type(ContactType::Social)->value('@northwind')->add();
```

- email(), phone(), url(), address() — set the kind and the value in one call.
- type(ContactType|string) + value(string) — set them separately; a registered custom kind keeps its raw name, and without a type the kind is custom.
- label(), name(), category(), meta(array) — descriptive fields.
- primary(bool $primary = true) — request primary status.
- structuredAddress(AddressData|array) — attach a structured address (see Structured addresses). A builder with no kind chosen becomes an address; an explicitly chosen other kind — type('whatsapp'), email() — is refused on add() with InvalidContactValue, whichever order you call them in.
- add() — terminal: validates, saves and returns the Contact.

## ContactData

ContactData is the readonly DTO every write accepts. Build it with named arguments or from an array; normalized() returns a normalized copy:

```php
use RoundlyConsulting\Contacts\DataTransferObjects\ContactData;
use RoundlyConsulting\Contacts\Enums\ContactType;

$data = new ContactData(
    type: ContactType::Phone,
    value: '+421 900 000 000',
    label: 'Office',
    name: null,
    category: 'Headquarters',
    isPrimary: false,
    position: null,           // null = next free position within the kind
    meta: ['ext' => '204'],
    kind: null,               // null = the type's own value; set it for custom kinds
);

$data = ContactData::fromArray([
    'type' => 'phone',        // ContactType or string
    'value' => '+421 900 000 000',
    'is_primary' => true,     // or 'isPrimary'
]);

$data->normalized()->value;   // '+421900000000'
```

## Updating and deleting

```php
use RoundlyConsulting\Contacts\Models\Contact;

$updated = Contacts::update($contact, new ContactData(
    ContactType::Email,
    'jane.doe@example.com',
    label: 'Work',
));

$updated->isVerified();                        // false — a new value needs a new verification

Contacts::delete($contact);                    // soft delete + ContactDeleted
Contact::withTrashed()->find($contact->id)->restore();   // addresses + connections are back
```

update() re-normalizes and re-validates the value. The kind, value, label, category and meta are replaced with the DTO’s values — a null label or category clears it — while name and position change only when set. A changed value or kind drops the verification and voids any pending token. Passing isPrimary: true promotes the contact.

Moving a contact to another kind keeps one primary per kind: a primary entering a kind that already has one is demoted (unless isPrimary asks for it, which demotes the other instead). With auto\_primary on, the kind it left promotes its next contact, and a kind it enters without a primary gets one. Deleting a primary promotes the next contact of its kind the same way.

## Low-level access

```php
// The raw relation still works — but it skips normalization, validation,
// auto-primary and events. Prefer the trait, the facade or the builder.
$user->contacts()->create([
    'type' => 'email',
    'name' => 'Jane Doe',
    'value' => 'jane@example.com',
]);
```

The contacts() relation, the scopes, the meta collection cast and direct create() calls all still work. Normalization, validation and auto-primary apply only through the action, facade and trait-sugar path. Dropping the verification on a value change applies everywhere, direct writes included.

## Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

[More ways to support, including crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/contacts-for-laravel.md)
