Turnkey Integration of Google & Outlook Contact Import

Problem: user manually enters 5000 contacts, CRM is empty

Development and maintenance of all types of websites:

Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:

Frequently Asked Questions

Latest works

  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1281
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1237
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    977
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    1025
  • image_website-sbh_0.webp
    Website development for SBH Partners
    1103
  • image_website-_0.webp
    Website development for Red Pear
    550

Problem: user manually enters 5000 contacts, CRM is empty

Typical scenario: HR portals, networking platforms, and CRM systems require the user to upload contacts. Manual entry of 5000+ records takes hours. Errors are inevitable, duplicates multiply. Imagine: a client spends 3 hours filling out a form, only to find half the contacts already exist. Importing from Google Contacts or Outlook solves this pain. But the implementation requires careful handling of OAuth, pagination, and different APIs. We implement the import turnkey: the user authorizes, selects the desired contacts, and imports them in two clicks. As a result, the database fills in minutes, and registration conversion grows by 30%. Contact us for a consultation on your project.

What technical difficulties we solve

Different authorization protocols

Google uses OAuth 2.0 with Google Identity Platform, Microsoft uses OAuth 2.0 with Azure AD. Each has its own endpoints, scopes, and token acquisition procedure. An error in the redirect URI configuration – and the user sees a blank screen.

Pagination with thousands of records. A user may have 5000+ contacts, but the API returns a maximum of 1000 per request. You need to correctly handle nextPageToken / @odata.nextLink. Without pagination, the import stops at the first batch.

Token refresh. An access token lives for 1 hour (Google) or 90 minutes (Outlook). A refresh token allows obtaining a new one – it must be stored encrypted in the database and updated on schedule. If this is not done, the import breaks after an hour.

UI selection and responsiveness. A list of 5000 contacts should not slow down the interface. We use virtualization or step-by-step loading. Otherwise, the browser freezes for 10 seconds.

Why Laravel is suitable for integration? + Complexity comparison

Laravel 11 provides built-in OAuth support via Socialite, ready-made encryption, and queues for background tasks. On one project (HR portal), we integrated import from Google and Outlook using the official SDKs – google/apiclient and microsoft/microsoft-graph. Both SDKs support refresh. Below are the key fragments.

Google People API: OAuth setup

In Google Cloud Console: create a project → enable "People API" → create 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 the token for the user return $token; } } 

Fetching 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 may have 5000+ contacts, the API returns a maximum of 1000 per request.

Microsoft Graph API: Outlook/Office 365 contacts

Register the 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 receiving 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> ); } 

Storing tokens

Access tokens must not be stored in the session – they should 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', ]; 

How the user imports contacts: step by step

  1. The user clicks "Import contacts" on the site.
  2. Selects a provider (Google or Outlook).
  3. The system redirects to the provider's OAuth page.
  4. The user grants permission to read contacts.
  5. The callback saves the tokens in the database.
  6. The frontend loads the contact list (with pagination).
  7. The user marks the desired contacts and clicks "Import".
  8. The selected contacts are saved to the CRM/site database.
Example of Google OAuth setupIn Google Cloud Console, create a project, enable People API, set up OAuth consent screen. Then create OAuth 2.0 Web application credentials, specifying the redirect URI to your server. Use the obtained client ID and secret in the Laravel configuration.

What is included in the work

Stage What we do Result
Analytics Agree on the list of providers, scopes, UI design Technical specification
Design Develop OAuth scheme, token storage, error handling Architecture documentation
Implementation Write services for Google and Outlook, frontend component Working import
Testing Check pagination, token refresh, edge cases Test report
Deployment Deploy to production, configure monitoring Access credentials, manual
Support 30 days of post-launch support Training, bug fixes

Complexity comparison: Google People API vs Microsoft Graph API

Parameter Google People API Microsoft Graph API
App registration Google Cloud Console Azure AD App Registrations
Max contacts per request 1000 (pageSize) 1000 ($top)
Pagination nextPageToken @odata.nextLink
Refresh token By default (access_type=offline) Need to request offline_access
SDK google/apiclient microsoft/microsoft-graph
Integration complexity Medium – 1.5x simpler than Outlook Higher – more Azure AD settings

Google is easier to start – integration takes 1.5 times less time. But Outlook is the standard in the corporate sector. We connect both.

Why trust us with the integration?

Our team has over 10 years of experience in web development and more than 50 projects with external API integrations. We are certified as Google Cloud Partner and have experience with Azure AD. We use secure practices for token storage, apply encryption, and regularly test token refresh. Automating contact import saves up to 90% of manual entry time, reducing costs by up to $2000 per year for a typical HR portal. Contact us for a project assessment – we will select the optimal solution and give exact timelines.

Estimated timelines and pricing

  • One provider (Google or Outlook) – from 2 to 3 working days, cost from $500.
  • Both providers with sync support – from 4 to 5 working days, cost from $900.
  • Additional features (custom UI, duplicate detection) – quoted individually.

Order a consultation – we will send a commercial proposal within one working day.