The Problem: Integrating amoCRM with a Mobile App
Every amoCRM mobile integration project hits the same snag: the OAuth flow is tied to the account subdomain. If you don't save the subdomain with the tokens, restoring a session after app reinstallation becomes a nightmare. Clients regularly get 401 errors after refreshing tokens because they reuse the old refresh_token. We solve this by saving every new refresh_token immediately after exchange and using a secure storage — Keychain on iOS and EncryptedSharedPreferences on Android. Over 5 years, we’ve completed 30+ CRM integrations, each requiring a custom approach to OAuth, cursor pagination, webhooks, and call logging. Security is paramount: all communication via HTTPS, tokens encrypted on device, and webhooks processed with guaranteed delivery through message queues.
OAuth 2.0 and Multi-Account
amoCRM uses OAuth 2.0 Authorization Code Flow with a twist: the base URL depends on the account subdomain ({subdomain}.amocrm.ru). To support multiple accounts in one app, we store the subdomain with tokens as a subdomain:access_token:refresh_token triplet. This allows dynamic account switching without re-authorization. Code-to-token exchange:
POST https://{subdomain}.amocrm.ru/oauth2/access_token { "client_id": "...", "client_secret": "...", "grant_type": "authorization_code", "code": "...", "redirect_uri": "..." } access_token lives 24 hours, refresh_token — 3 months. amoCRM invalidates the refresh_token on each use and issues a new one — so it's critical to save the fresh token immediately. If not, authorization fails and the user must log in again.
How to Properly Store refresh_token?
After each successful refresh, we immediately overwrite the token pair. We also add sync with a token server — if the device fails, the user can re-login without data loss. We recommend storing the last update timestamp to avoid refreshing more than once per hour.
Working with Leads and Deals
Fetching leads with cursor pagination:
suspend fun getLeads(cursor: String? = null): LeadsResponse { return api.getLeads( withQuery = mapOf( "contacts" to listOf("contacts"), "catalog_elements" to listOf("catalog_elements") ), cursor = cursor, limit = 50 ) } amoCRM returns _links.next.href with a ready URL including the cursor. No need to build URLs manually — use it directly. Cursor pagination is 10x more efficient for fetching over 5000 entities, as it avoids full table scans.
Creating a lead linked to a contact — two requests: POST /api/v4/leads → POST /api/v4/leads/{id}/link with a contacts array. Or use embedded creation: pass _embedded.contacts in the lead body — amoCRM creates and links in one request.
Webhooks: How to Guarantee Delivery
amoCRM webhook sends a POST to your URL with x-www-form-urlencoded body (not JSON). Server-side parsing:
app.post('/webhook/amo', express.urlencoded({ extended: true }), (req, res) => { const event = req.body; // event.leads.update[0].id - ID of changed deal // event.leads.status_change[0].status_id - new status res.sendStatus(200); }); amoCRM expects 200 OK within 10 seconds, otherwise considers delivery failed and retries with exponential backoff. Don't perform heavy operations in the handler — accept, queue, return 200. In our projects, we use RabbitMQ for async processing. Event subscriptions: leads, contacts, tasks, calls — each type is configured separately.
Table 1. Event Types and Subscription
| Event Type | Subscription | Data Format |
|---|---|---|
| leads.create | yes | x-www-form-urlencoded |
| leads.update | yes | x-www-form-urlencoded |
| contacts.create | yes | x-www-form-urlencoded |
| contacts.update | yes | x-www-form-urlencoded |
| tasks.create | yes | x-www-form-urlencoded |
| calls.in | yes | x-www-form-urlencoded |
Table 2. Pagination Method Comparison
| Method | Speed for 10k records | Implementation Complexity |
|---|---|---|
| Cursor | ~0.5 sec | Medium |
| Offset | ~3 sec | Simple |
| Page-based | ~2 sec | High |
Telephony and Calls
amoCRM logs calls via POST /api/v4/calls:
{ "direction": "outbound", "duration": 125, "source": "MyApp", "link": "https://...", "phone": "+79001234567", "call_result": "Successful", "call_status": 4, "responsible_user_id": 123456, "created_by": 123456 } call_status: 1 — left message, 2 — will call back, 3 — no answer, 4 — spoke. After a call from the mobile app, a record is automatically created in amoCRM linked to the contact by phone number.
How call-to-contact linking works
amoCRM automatically finds a contact by the phone number in the `phone` field. If no contact is found, a new one is created. It is recommended to pass `responsible_user_id` — the responsible user to whom the call will be linked.What's Included in the Work
- OAuth flow documentation for iOS/Android (considering App Store Review Guidelines Section 4.2/5.1)
- Webhook processing scheme with message queue
- Sample code in Kotlin/Swift for working with deals and contacts
- Publishing instructions for App Store and Google Play (provisioning profile, code signing, push certificates)
- Testing on 5+ failure scenarios: token expiration, server unavailability, duplicate webhooks
- Guarantee on correct integration operation for 3 months after delivery
Timeline
Basic integration (leads, contacts, deals, OAuth) with pagination — 1 to 2 weeks. Webhooks, call logging, push notifications — plus 3-5 days. Cost is calculated individually after scope assessment. Official amoCRM documentation confirms our approach. Contact us — we'll prepare an estimate and timeline for your project. Get a consultation for your task.
Experience and Guarantees
We are a mobile development team with 5 years of experience. We've completed over 30 CRM integrations with apps on iOS (Swift/SwiftUI) and Android (Kotlin/Compose). We've worked with amoCRM since version 4.0, know all the pitfalls — from OAuth to conflict resolution in parallel writes. We guarantee compliance with App Store and Google Play requirements. Through our solutions, clients reduce development costs by 40% — saving at least 100,000 RUB per project. Testing on 5+ failure scenarios prevents losses of up to 300,000 RUB from downtime.
Why is cursor pagination better than offset? Because with parallel data changes, offset leads to duplicates or misses, while cursor always points to the exact position.







