amoCRM Integration with Website
amoCRM focuses on sales departments: pipeline, contacts, tasks, chats. REST API covers all main entities. Authentication is only via OAuth 2.0 (not webhook-tokens like in Bitrix24), requiring app setup and refresh-token storage.
OAuth 2.0 for Server-to-Server
amoCRM uses Authorization Code flow with long-lived refresh token (60 days). Algorithm:
- Create integration in amoCRM account:
Settings → Integrations → Create Integration - Get
client_id,client_secret,redirect_uri - Initial authorization — manual step via browser (code generation)
- Exchange code for access + refresh tokens — programmatically
- Automatic access-token refresh via refresh
class AmoCrmTokenStorage {
private const CACHE_KEY = 'amocrm_tokens';
public function getAccessToken(): string {
$tokens = Cache::get(self::CACHE_KEY);
if (!$tokens || now()->gte($tokens['expires_at'])) {
$tokens = $this->refreshTokens($tokens['refresh_token']);
}
return $tokens['access_token'];
}
private function refreshTokens(string $refreshToken): array {
$response = Http::post(config('amocrm.base_url') . '/oauth2/access_token', [
'client_id' => config('amocrm.client_id'),
'client_secret' => config('amocrm.client_secret'),
'grant_type' => 'refresh_token',
'refresh_token' => $refreshToken,
'redirect_uri' => config('amocrm.redirect_uri'),
]);
$data = $response->json();
$tokens = [
'access_token' => $data['access_token'],
'refresh_token' => $data['refresh_token'],
'expires_at' => now()->addSeconds($data['expires_in'] - 60),
];
Cache::put(self::CACHE_KEY, $tokens, now()->addDays(60));
// Also save to DB as backup (cache may clear)
DB::table('settings')->updateOrInsert(
['key' => 'amocrm_tokens'],
['value' => json_encode($tokens), 'updated_at' => now()]
);
return $tokens;
}
}
API Client
class AmoCrmClient {
private AmoCrmTokenStorage $tokenStorage;
private string $baseUrl;
public function __construct(AmoCrmTokenStorage $tokenStorage) {
$this->tokenStorage = $tokenStorage;
$this->baseUrl = rtrim(config('amocrm.base_url'), '/') . '/api/v4';
}
public function get(string $endpoint, array $params = []): array {
return Http::withToken($this->tokenStorage->getAccessToken())
->timeout(15)
->get($this->baseUrl . $endpoint, $params)
->throw()
->json();
}
public function post(string $endpoint, array $data): array {
return Http::withToken($this->tokenStorage->getAccessToken())
->timeout(15)
->post($this->baseUrl . $endpoint, $data)
->throw()
->json();
}
public function patch(string $endpoint, array $data): array {
return Http::withToken($this->tokenStorage->getAccessToken())
->timeout(15)
->patch($this->baseUrl . $endpoint, $data)
->throw()
->json();
}
}
Creating Contact and Deal
amoCRM is built on three main entities: Contact, Lead (deal), Company. New lead from website — is a Contact + Lead pair:
class AmoCrmLeadService {
public function createLeadFromForm(array $formData): array {
$amo = app(AmoCrmClient::class);
// Search for existing contact by phone
$existing = $amo->get('/contacts', [
'query' => $formData['phone'],
'with' => 'leads',
]);
$contactId = null;
if (!empty($existing['_embedded']['contacts'])) {
$contactId = $existing['_embedded']['contacts'][0]['id'];
}
// Create contact if not found
if (!$contactId) {
$result = $amo->post('/contacts', [[
'name' => $formData['name'],
'custom_fields_values' => [
[
'field_code' => 'PHONE',
'values' => [['value' => $formData['phone'], 'enum_code' => 'WORK']],
],
[
'field_code' => 'EMAIL',
'values' => [['value' => $formData['email'] ?? '', 'enum_code' => 'WORK']],
],
],
]]);
$contactId = $result['_embedded']['contacts'][0]['id'];
}
// Create deal
$pipelineId = config('amocrm.pipeline_id');
$stageId = config('amocrm.initial_stage_id');
$leadResult = $amo->post('/leads', [[
'name' => 'Request from website: ' . $formData['name'],
'pipeline_id' => $pipelineId,
'status_id' => $stageId,
'_embedded' => [
'contacts' => [['id' => $contactId]],
],
'custom_fields_values' => [
[
'field_id' => config('amocrm.fields.source'),
'values' => [['value' => $formData['source'] ?? 'Website']],
],
[
'field_id' => config('amocrm.fields.utm_campaign'),
'values' => [['value' => session('utm_campaign') ?? '']],
],
],
]]);
$leadId = $leadResult['_embedded']['leads'][0]['id'];
// Add note
$amo->post('/leads/' . $leadId . '/notes', [[
'note_type' => 'common',
'params' => ['text' => 'Message: ' . ($formData['message'] ?? '')],
]]);
return ['lead_id' => $leadId, 'contact_id' => $contactId];
}
}
Webhook from amoCRM
Webhook setup in amoCRM: Settings → Integrations → Webhooks. When deal status changes, amoCRM sends POST:
// routes/api.php
Route::post('/webhooks/amocrm', [AmoCrmWebhookController::class, 'handle']);
class AmoCrmWebhookController extends Controller {
public function handle(Request $request): Response {
// amoCRM sends form-encoded data, not JSON!
$data = $request->all();
if (isset($data['leads']['status'])) {
foreach ($data['leads']['status'] as $lead) {
$this->syncLeadStatus($lead['id'], $lead['status_id']);
}
}
if (isset($data['leads']['add'])) {
// New deal created in amoCRM (not from website)
foreach ($data['leads']['add'] as $lead) {
Log::info('New lead in amoCRM', ['id' => $lead['id']]);
}
}
return response('ok');
}
private function syncLeadStatus(int $leadId, int $statusId): void {
$order = Order::where('amocrm_lead_id', $leadId)->first();
if (!$order) return;
$map = [
config('amocrm.stages.won') => 'completed',
config('amocrm.stages.lost') => 'cancelled',
config('amocrm.stages.in_work') => 'processing',
];
if ($status = $map[$statusId] ?? null) {
$order->update(['status' => $status]);
}
}
}
Custom Fields
amoCRM supports custom fields for deals and contacts. Field IDs must be retrieved via API and saved in config:
// Get list of pipeline fields
$fields = $amo->get('/leads/custom_fields');
foreach ($fields['_embedded']['custom_fields'] as $field) {
echo $field['id'] . ' — ' . $field['name'] . PHP_EOL;
}
Then in config/amocrm.php:
'fields' => [
'source' => 12345,
'utm_source' => 12346,
'utm_campaign' => 12347,
'order_number' => 12348,
],
Updating Deal on Payment
add_action('order_paid', function(Order $order) {
if (!$order->amocrm_lead_id) return;
app(AmoCrmClient::class)->patch('/leads/' . $order->amocrm_lead_id, [
'price' => $order->total,
'status_id' => config('amocrm.stages.won'),
'custom_fields_values' => [
[
'field_id' => config('amocrm.fields.order_number'),
'values' => [['value' => $order->number]],
],
],
]);
});
Implementation Timeline
OAuth authorization, basic lead sending from forms, token storage: 1–2 days. Full integration: bidirectional status synchronization, webhook handler, custom fields, UTM tags: 3–4 days. Catalog synchronization, task creation on website events, chat integration: plus 2–3 days.







