-
Notifications
You must be signed in to change notification settings - Fork 2
AircallAPI
This page is the integration cookbook for the Aircall service inside Pair applications. Use it after reading AircallClient, which is the class reference.
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
The current AircallClient behavior matters for production design:
- it authenticates with Basic Auth using
AIRCALL_API_IDandAIRCALL_API_TOKEN - it retries cURL transport failures and transient
5xxresponses according toAIRCALL_MAX_RETRIES - it does not auto-retry
429; instead it stores a local cooldown marker and throwsAircallRateLimitException - it maps
404,410, and422toAircallResourceUnavailableException - it honors
Retry-Afterwhen Aircall sends it - it normalizes boolean query parameters recursively to the strings
trueandfalse - 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.
A clean Pair implementation usually has three layers:
- a service class that wraps
AircallClientand maps Aircall behavior to domain outcomes - controllers or API endpoints that validate input and call the service
- 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;
}
}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
maxPagesexplicit - persist checkpoints such as
last_synced_at - never run full-account imports synchronously inside a web request
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.
$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.
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.',
]);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.
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.
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, transient5xx - handle gracefully:
404,410,422 - fail fast: credentials, malformed payloads, unsupported endpoint assumptions
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
- keep credentials only in
.envor a secret manager - tune timeout and retry values to the real workload
- monitor sustained
429or repeated5xx - make webhook consumers idempotent
- move heavy Aircall sync out of request/response endpoints