Skip to content

AircallAPI

Viames Marino edited this page Mar 26, 2026 · 2 revisions

Pair framework: Aircall API integration

This page is the integration cookbook for the Aircall service inside Pair applications. Use it after reading AircallClient, which is the class reference.

Scope

Use this integration when backend code must:

  • sync contacts, calls, users, teams, or numbers with another system
  • consume Aircall webhooks
  • send messages from an Aircall number
  • provision users or team membership from Pair
  • run scheduled jobs that page through Aircall data safely

What the current Pair implementation actually does

The current AircallClient behavior matters for production design:

  • it authenticates with Basic Auth using AIRCALL_API_ID and AIRCALL_API_TOKEN
  • it retries cURL transport failures and transient 5xx responses according to AIRCALL_MAX_RETRIES
  • it does not auto-retry 429; instead it stores a local cooldown marker and throws AircallRateLimitException
  • it maps 404, 410, and 422 to AircallResourceUnavailableException
  • it honors Retry-After when Aircall sends it
  • it normalizes boolean query parameters recursively to the strings true and false
  • OAuth-only integration endpoints are intentionally not supported through Basic Auth and raise PairException

Those details are why a good Aircall integration should stop batches on rate limit, distinguish missing resources from generic failures, and keep request-side controllers thin.

Recommended project structure

A clean Pair implementation usually has three layers:

  1. a service class that wraps AircallClient and maps Aircall behavior to domain outcomes
  2. controllers or API endpoints that validate input and call the service
  3. scheduled jobs or queue workers for heavy sync and retry flows

Example service shell:

namespace App\Services;

use Pair\Services\AircallClient;

class AircallSyncService {

    public function __construct(
        private readonly AircallClient $aircall = new AircallClient()
    ) {}

    public function syncContactsWindow(string $from, string $to): int
    {
        $rows = $this->aircall->getAllContacts([
            'from' => $from,
            'to' => $to,
            'per_page' => 100,
        ], maxPages: 20);

        $count = 0;

        foreach ($rows as $row) {
            $remoteId = (int)($row['id'] ?? 0);

            if ($remoteId < 1) {
                continue;
            }

            // upsert locally
            $count++;
        }

        return $count;
    }
}

Integration recipes

1) Resilient incremental sync job

This is the most common pattern for cron or queue workers:

use App\Services\AircallSyncService;
use Pair\Exceptions\AircallRateLimitException;
use Pair\Exceptions\AircallResourceUnavailableException;
use Pair\Exceptions\PairException;
use Pair\Services\AircallClient;

$cooldownUntil = AircallClient::activeLocalCooldownUntil();

if ($cooldownUntil > time()) {
    return;
}

try {
    $synced = (new AircallSyncService())->syncContactsWindow('2026-03-01', '2026-03-26');
} catch (AircallRateLimitException $e) {
    // stop immediately and let the scheduler retry later
} catch (AircallResourceUnavailableException $e) {
    // resource disappeared or is currently unavailable; log and continue with next unit
} catch (PairException $e) {
    // credentials, invalid payload, invalid JSON, generic API error
    throw $e;
}

Recommended safeguards:

  • always sync a bounded time window when the Aircall endpoint supports it
  • keep maxPages explicit
  • persist checkpoints such as last_synced_at
  • never run full-account imports synchronously inside a web request

2) Webhook receiver with idempotent handoff

Webhook endpoints should acknowledge fast and offload heavy work:

use Pair\Api\ApiController as BaseApiController;
use Pair\Api\ApiResponse;

class ApiController extends BaseApiController {

    public function aircallWebhookAction(): void
    {
        $this->requireJsonPost();
        $payload = $this->request->json();

        $event = (string)($payload['event'] ?? '');
        $eventId = (string)($payload['id'] ?? '');

        if ($event === '' || $eventId === '') {
            ApiResponse::error('BAD_REQUEST', ['detail' => 'Invalid payload']);
        }

        // Persist the event first, reject duplicates by eventId, enqueue heavy work.
        ApiResponse::respond(['accepted' => true], 202);
    }
}

Practical rule: store the raw payload and deduplicate by Aircall event ID before running downstream business logic.

3) Provision a user and attach the user to a team

$newUser = $aircall->createUserV2([
    'first_name' => 'Jane',
    'last_name' => 'Doe',
    'email' => 'jane.doe@example.com',
]);

$userId = (int)($newUser['user']['id'] ?? 0);

if ($userId > 0) {
    $team = $aircall->createTeam(['name' => 'Sales EMEA']);
    $teamId = (int)($team['team']['id'] ?? 0);

    if ($teamId > 0) {
        $aircall->addUserToTeam($teamId, $userId);
    }
}

This works well when Pair is the system of record for team membership and Aircall is a downstream execution platform.

4) Messaging workflow with explicit configuration

If your app sends messages through Aircall numbers, configure the number once and then send messages from business events:

$aircall->createMessageNumberConfiguration(123456, [
    'enabled' => true,
]);

$aircall->sendMessage(123456, [
    'to' => '+12025550100',
    'content' => 'Your request has been processed.',
]);

For internal agent flows:

$aircall->sendMessageInAgentConversation(123456, [
    'conversation_id' => 'abc123',
    'content' => 'Internal follow-up sent.',
]);

5) Webhook bootstrap and reconciliation

A common deployment pattern is to reconcile webhooks instead of blindly recreating them:

$existing = $aircall->getAllWebhooks(['per_page' => 100], maxPages: 10);
$targetUrl = 'https://example.com/api/aircall/webhook';
$match = null;

foreach ($existing as $hook) {
    if (($hook['url'] ?? '') === $targetUrl) {
        $match = $hook;
        break;
    }
}

if (!$match) {
    $aircall->createWebhook([
        'url' => $targetUrl,
        'events' => ['call.created', 'call.ended'],
    ]);
}

This avoids duplicate webhook registrations during repeated deployments.

6) Raw endpoint fallback for newly released Aircall routes

When Aircall adds an endpoint before Pair wraps it, raw() is the escape hatch:

$response = $aircall->raw('GET', '/v2/users', ['page' => 1]);

Use this sparingly and move to a typed helper when the behavior becomes core to your app.

Error handling strategy

Recommended catch order:

use Pair\Exceptions\AircallRateLimitException;
use Pair\Exceptions\AircallResourceUnavailableException;
use Pair\Exceptions\PairException;

try {
    $contact = $aircall->getContact(123);
} catch (AircallRateLimitException $e) {
    // stop the batch, reschedule later
} catch (AircallResourceUnavailableException $e) {
    // mark the remote record as missing, stale, or not ready
} catch (PairException $e) {
    // credentials, malformed request, invalid JSON, generic API error
    throw $e;
}

A good rule of thumb:

  • retry later: 429, cURL/network failures, transient 5xx
  • handle gracefully: 404, 410, 422
  • fail fast: credentials, malformed payloads, unsupported endpoint assumptions

Pagination and sync safeguards

Keep these habits in every Aircall integration:

  • bound maxPages
  • use time windows where available
  • keep a local checkpoint or cursor
  • log remote IDs for traceability
  • redact tokens and PII from operational logs where required

Production checklist

  • keep credentials only in .env or a secret manager
  • tune timeout and retry values to the real workload
  • monitor sustained 429 or repeated 5xx
  • make webhook consumers idempotent
  • move heavy Aircall sync out of request/response endpoints

Related pages

Clone this wiki locally