Implementing contact import (Google/Outlook) on a website
Importing contacts from Google or Outlook to a website is a typical task for CRM systems, HR portals, and networking platforms. The user authenticates via OAuth, grants permission to read contacts, the site retrieves the list and offers the user the choice to invite or synchronize selected contacts. Technically, Google and Outlook are fundamentally different APIs unified by a single UI flow.
Google Contacts API: OAuth setup
In Google Cloud Console: create a project → enable "People API" → create an OAuth 2.0 Client ID (type: Web application) → add redirect URI.
Required scopes:
-
https://www.googleapis.com/auth/contacts.readonly— read contacts -
https://www.googleapis.com/auth/contacts.other.readonly— contacts from "Other contacts"
use Google\Client as GoogleClient;
class GoogleContactsService
{
private GoogleClient $client;
public function __construct()
{
$this->client = new GoogleClient();
$this->client->setClientId(config('services.google.client_id'));
$this->client->setClientSecret(config('services.google.client_secret'));
$this->client->setRedirectUri(config('services.google.redirect'));
$this->client->addScope('https://www.googleapis.com/auth/contacts.readonly');
$this->client->setAccessType('offline'); // get refresh_token
}
public function getAuthUrl(): string
{
return $this->client->createAuthUrl();
}
public function handleCallback(string $code): array
{
$token = $this->client->fetchAccessTokenWithAuthCode($code);
// Save token for user
return $token;
}
}
Getting contacts from Google People API
public function getContacts(array $accessToken): array
{
$this->client->setAccessToken($accessToken);
if ($this->client->isAccessTokenExpired() && isset($accessToken['refresh_token'])) {
$this->client->fetchAccessTokenWithRefreshToken($accessToken['refresh_token']);
}
$service = new \Google\Service\PeopleService($this->client);
$contacts = [];
$pageToken = null;
do {
$params = [
'personFields' => 'names,emailAddresses,phoneNumbers',
'pageSize' => 1000,
];
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$result = $service->people_connections->listPeopleConnections('people/me', $params);
foreach ($result->getConnections() ?? [] as $person) {
$name = $person->getNames()[0] ?? null;
$email = $person->getEmailAddresses()[0] ?? null;
$phone = $person->getPhoneNumbers()[0] ?? null;
if (!$email) continue; // skip without email
$contacts[] = [
'name' => $name?->getDisplayName() ?? '',
'email' => $email->getValue(),
'phone' => $phone?->getValue() ?? '',
];
}
$pageToken = $result->getNextPageToken();
} while ($pageToken);
return $contacts;
}
Pagination is mandatory: a user might have 5000+ contacts, and the API returns a maximum of 1000 per request.
Microsoft Graph API: Outlook/Office 365 contacts
Register your application in Azure AD → "App registrations" → "New registration". Required permissions: Contacts.Read (Delegated).
use Microsoft\Graph\Graph;
use Microsoft\Graph\Model\Contact;
class OutlookContactsService
{
public function getAuthUrl(): string
{
$params = http_build_query([
'client_id' => config('services.microsoft.client_id'),
'response_type' => 'code',
'redirect_uri' => config('services.microsoft.redirect'),
'scope' => 'offline_access Contacts.Read',
'response_mode' => 'query',
]);
return "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?{$params}";
}
public function getToken(string $code): array
{
$response = Http::asForm()->post(
'https://login.microsoftonline.com/common/oauth2/v2.0/token',
[
'client_id' => config('services.microsoft.client_id'),
'client_secret' => config('services.microsoft.client_secret'),
'code' => $code,
'redirect_uri' => config('services.microsoft.redirect'),
'grant_type' => 'authorization_code',
]
);
return $response->json();
}
public function getContacts(string $accessToken): array
{
$graph = new Graph();
$graph->setAccessToken($accessToken);
$contacts = [];
$url = '/me/contacts?$select=displayName,emailAddresses,mobilePhone&$top=100';
do {
$result = $graph->createRequest('GET', $url)->execute();
$data = $result->getBody();
foreach ($data['value'] as $contact) {
$email = $contact['emailAddresses'][0]['address'] ?? null;
if (!$email) continue;
$contacts[] = [
'name' => $contact['displayName'] ?? '',
'email' => $email,
'phone' => $contact['mobilePhone'] ?? '',
];
}
$url = $data['@odata.nextLink'] ?? null;
// Remove base URL for Graph SDK
if ($url) {
$url = str_replace('https://graph.microsoft.com/v1.0', '', $url);
}
} while ($url);
return $contacts;
}
}
UI: selecting contacts for import
After retrieving the list, the user selects which contacts to import:
function ContactImportModal({ contacts, onImport }) {
const [selected, setSelected] = useState(new Set());
const toggle = (email) => {
setSelected(prev => {
const next = new Set(prev);
next.has(email) ? next.delete(email) : next.add(email);
return next;
});
};
return (
<div>
<div className="actions">
<button onClick={() => setSelected(new Set(contacts.map(c => c.email)))}>
Select all ({contacts.length})
</button>
</div>
<ul>
{contacts.map(contact => (
<li key={contact.email}>
<label>
<input
type="checkbox"
checked={selected.has(contact.email)}
onChange={() => toggle(contact.email)}
/>
{contact.name} — {contact.email}
</label>
</li>
))}
</ul>
<button onClick={() => onImport([...selected])}>
Import selected ({selected.size})
</button>
</div>
);
}
Token storage
Access tokens cannot be stored in sessions — they must be in the database, encrypted:
// Migration
$table->text('google_access_token')->nullable();
$table->text('google_refresh_token')->nullable();
$table->timestamp('google_token_expires_at')->nullable();
// In User model — automatic encryption
protected $casts = [
'google_access_token' => 'encrypted',
'google_refresh_token' => 'encrypted',
];
Timeline
Importing from a single provider (Google or Outlook) with contact selection UI, OAuth flow, and database storage: 2–3 working days. Both providers simultaneously with token updates and re-synchronization: 4–5 working days.







