Turnkey Integration of Google & Outlook Contact Import

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.

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.

Showing 1 of 1All 2062 services
Turnkey Integration of Google & Outlook Contact Import
Medium
from 1 day to 3 days
Frequently Asked Questions

Our competencies:

Development stages

Latest works

  • image_website-b2b-advance_0.webp
    B2B ADVANCE company website development
    1358
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1250
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    956
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1188
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    929
  • image_bitrix-bitrix-24-1c_fixper_448_0.webp
    Website development for FIXPER company
    947

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.

Website CRM Integration: Bitrix24, amoCRM, Salesforce, HubSpot

A sales manager manually copies leads from email into the CRM. Half of them never make it. Follow‑up calls are missed. This isn’t a people problem — it’s an architectural gap between the website and the company’s core system. We close that gap with a direct site‑to‑CRM connection: leads land in the pipeline within 30 seconds after form submission, duplication is blocked, and status changes flow both ways automatically. Request a free integration audit to identify the bottlenecks in your current flow.

Integration isn’t just a POST to an API endpoint. It’s a battle against timeouts, duplicate records, data loss, and desynchronised states. We handle three core problems at once: asynchronous delivery (so the user never waits for the CRM), deduplication by email (one address – one lead), and two‑way feedback (a status change in the CRM instantly appears on the site). Below is how we tackle each.

Bitrix24: REST API and Event Handlers

Bitrix24 dominates the Russian B2B space. Its REST API works via OAuth 2.0 or an incoming webhook (webhook is simpler but less secure for production). Main entities are lead, deal, contact, and company.

Creating a lead requires POST /rest/crm.lead.add with the correct field set. Attaching it to a funnel means passing SOURCE_ID. Adding a timeline comment uses crm.timeline.comment.add. Real‑time tracking is done through Event Handlers: register a hook with event.bind; Bitrix24 pushes a POST to your endpoint when any deal status changes.

The real complexity lies in custom fields. Every Bitrix24 installation has its own set, and their IDs must be fetched via crm.lead.fields. Mapping those fields between the site and the CRM can be done manually or automatically — we use an automatic detection mechanism that works even in non‑standard configurations (proven on 20+ projects). We guarantee correct matching, so no lead arrives without the right pipeline stage or source tag.

amoCRM: Clean REST with Predictable Endpoints

amoCRM (now Kommo for international markets) offers a cleaner API. OAuth 2.0 with refresh token, JSON API, and well‑structured endpoints. Pipelines are pipelines, deals are leads, contacts are contacts.

A common mistake: when creating a deal you must supply pipeline_id and status_id explicitly. Without them the deal lands in the default pipeline – often the wrong one. Tags for source classification go into _embedded.tags. Incoming webhooks are configured in the admin panel; they support add, update, delete, status, and note events. We always verify the webhook signature using the API key and make sure the endpoint responds with 200 OK in under 5 seconds – otherwise the CRM marks delivery as failed.

Salesforce and HubSpot: Enterprise‑Grade Integration

Salesforce is the enterprise standard. It offers REST API, SOQL for complex queries, and Apex for server‑side logic. Integration can be direct via Salesforce REST API or through middleware like Zapier or MuleSoft. For PHP projects we use phpforce/soap-client or the Force.com‑Toolkit. The main challenge is mapping hundreds of custom objects and fields; we solve it with Describe Global to collect metadata automatically – cutting setup time by three‑quarters compared to reading documentation manually (Salesforce Developer Guide).

HubSpot is popular among SaaS companies and international B2B. Its API v3 provides a REST interface with solid SDKs for PHP and Node.js (@hubspot/api-client). Contacts, Companies, Deals are standard objects. The Forms API lets you send data from any custom form directly to HubSpot without using the native widget. One pitfall: the access_token must include the right scopes; a misconfigured token returns 403 Forbidden with a vague message. We include error_logging that captures the error code – debugging takes minutes instead of hours.

Which CRM fits your business: Bitrix24, amoCRM, or HubSpot?

Criteria Bitrix24 amoCRM HubSpot
API complexity Medium (REST + webhooks, custom fields) Low (clean JSON API) Medium (REST + SDK, OAuth 2.0)
Typical synchronous latency 200‑600 ms 100‑300 ms 150‑400 ms
Built‑in deduplication by email crm.duplicate.findByComm Contact search contacts/search
Webhook events Event Handlers (push) Admin panel configuration Webhook + Automations
Best suited for Russian B2B, government, custom fields Small‑ to medium‑sized business International B2B, SaaS

Why is asynchronous sending important?

Calling a CRM API synchronously from the form handler is a mistake. The API may respond in 2 seconds – or time out. The user sits waiting. The correct pattern: form submission → save to database → queue a job → return 200 to the user immediately. A background worker then pushes the lead to the CRM. If the CRM is down, the worker retries with exponential backoff. We use Redis + Bull on Node.js or Laravel Queue on PHP – this guarantees delivery even during temporary outages.

Deduplication – how we stop duplicate leads

The same contact may fill the form twice. Without deduplication the CRM ends up with two identical leads. Before creating a new lead we search by email: for Bitrix24 we call crm.duplicate.findByComm, for HubSpot we use contacts/search. If a match is found we attach a task or comment to the existing lead instead of creating a new one. In our projects this cuts duplicate entries by 95%.

Two‑way synchronization – what happens when a manager changes a deal status

If a manager updates a deal status in the CRM, the website needs to reflect that change – especially if the client has a personal account. We configure webhooks from the CRM to an endpoint on the site, then update the local database and notify the client. Critical details: verify the webhook signature and respond with 200 OK within 5 seconds, otherwise the CRM assumes delivery failed. We guarantee that the delay between a status change in the CRM and its appearance on the site never exceeds 3 seconds.

How do we conduct integration in 5 steps?

  1. Audit of data flows – analyse current lead transfer, CRM field structure, and performance bottlenecks. Deliverable: “as‑is” and “to‑be” data flow diagrams.
  2. Architecture design – choose the queue mechanism (Redis Bull or Laravel Queue), define the deduplication method, and prepare a field mapping specification.
  3. Implementation on staging – write code on Laravel or Node.js, configure webhooks, and test with real data: lead creation, status updates, and error handling.
  4. Load testing – simulate peak traffic (e.g. 500 requests per minute) and adjust retry policies and timeout settings.
  5. Deployment and documentation – push to production, train the team on monitoring and retry cleanup, and deliver full endpoint documentation.

What is included in the work

  • Audit report with current data flow diagrams and typical error patterns.
  • Architecture design document specifying queue, deduplication, and mapping.
  • Production‑ready integration code on Laravel or Node.js.
  • Webhook configuration and signature verification.
  • Team training on support tasks and retry cleanup.
  • 30‑day warranty support for bug fixes and mapping adjustments.

Real‑world case: real‑estate agency with 400 leads per month

Click to expand A real‑estate agency processed every incoming lead manually – 400 leads per month. Each lead took 3 minutes to enter, and 15% were lost because emails were missed. We integrated their site with amoCRM using asynchronous queue delivery and automatic deduplication. Leads now appear in the pipeline within 5 seconds, and leftover tasks are automatically assigned to the next available agent. Result: 30% increase in conversion and $12,000 saved annually in administrative overhead.

Timelines

Scenario Duration
One CRM, lead transfer from forms 1‑2 weeks
Two‑way synchronization + statuses 3‑5 weeks
Multiple CRM + custom field mapping 4‑8 weeks

The exact cost is calculated after an audit of your current processes and CRM data structure. Contact us for a project estimate – we will send a commercial proposal within one business day. With 5+ years of experience and more than 20 completed integrations, you get a solution that works from day one. Get an engineer consultation to see how your sales funnel can run without manual lead transfer.

Additional sources: Customer relationship management (Wikipedia) · REST API (Wikipedia)