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
- The user clicks "Import contacts" on the site.
- Selects a provider (Google or Outlook).
- The system redirects to the provider's OAuth page.
- The user grants permission to read contacts.
- The callback saves the tokens in the database.
- The frontend loads the contact list (with pagination).
- The user marks the desired contacts and clicks "Import".
- The selected contacts are saved to the CRM/site database.
Example of Google OAuth setup
In 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.







