# API CRM

Membuat dan memperbarui kontak, deal, dan tugas dari aplikasi Anda sendiri: formulir website, toko online, sistem ERP, atau AI agent.

---

## Sebelum mulai

- Alamat dasar, autentikasi, dan bentuk balasan sama dengan [Referensi API](REFERENSI_API.md).
- API key **wajib** diberi scope CRM secara eksplisit. Scope **Semua** tidak mencakup CRM, supaya kunci integrasi lama tidak tiba-tiba bisa mengubah data penjualan.

| Scope | Membuka |
|---|---|
| `crm:read` (CRM baca) | Membaca pipeline dan kontak |
| `crm:write` (CRM tulis) | Semua endpoint di halaman ini, termasuk membaca |

Atur scope saat membuat kunci di menu **API Keys**. Lihat [API Key & Keamanan](API_KEY.md).

- API CRM tidak membuat chat baru dan tidak mengirim pesan WhatsApp. Untuk mengirim pesan, pakai endpoint [Pesan](REFERENSI_API.md).
- Nomor telepon diterima dalam format `0812…`, `62812…`, atau `+62 812…`.
- Nilai uang dalam **rupiah bulat**, tanpa desimal.

### Galat khusus CRM

| Status | `error.code` | Artinya |
|---|---|---|
| `403` | `scope_missing` | Kunci tidak punya scope CRM yang dibutuhkan |
| `403` | `plan_feature_unavailable` | Paket workspace tidak menyertakan API CRM |
| `404` | `contact_not_found` | Kontak tidak ditemukan |
| `422` | `invalid_phone` | Nomor telepon tidak valid |
| `409` | bervariasi | Bentrok dengan perubahan lain, misalnya deal sudah ditutup. Ambil ulang datanya, lalu putuskan apakah perlu diulang |
| `422` | bervariasi | Data tidak valid. `error.fields` menunjukkan kolom yang bermasalah |

---

# Pipeline

## Daftar pipeline

`GET /api/v1/pipelines` · scope `crm:read`

Dipakai untuk mengetahui `pipeline_id` dan `stage_id` sebelum membuat atau memindahkan deal.

```bash
curl https://pesanvo.com/api/v1/pipelines -H "X-Api-Key: $KEY"
```

```json
{
  "success": true,
  "data": [
    {
      "id": "01K2PIPE...",
      "name": "Penjualan",
      "is_default": true,
      "archived": false,
      "stages": [
        { "id": 11, "name": "Prospek", "type": "open", "position": 1, "probability": 10, "rot_days": 7 },
        { "id": 12, "name": "Penawaran", "type": "open", "position": 2, "probability": 40, "rot_days": 5 },
        { "id": 15, "name": "Berhasil", "type": "won", "position": 5, "probability": 100, "rot_days": null },
        { "id": 16, "name": "Gagal", "type": "lost", "position": 6, "probability": 0, "rot_days": null }
      ]
    }
  ]
}
```

`type` bernilai `open`, `won`, atau `lost`.

---

# Kontak

## Buat atau perbarui kontak

`POST /api/v1/contacts/upsert` · scope `crm:write`

Kontak dicari berdasarkan nomor. Bila belum ada, kontak dibuat. Bila sudah ada, kolom yang Anda kirim diperbarui. Aman dipanggil berulang kali.

| Parameter | Tipe | Wajib | Keterangan |
|---|---|---|---|
| `phone` | string | ya | Nomor telepon |
| `name` | string, maks 120 | tidak | |
| `email` | string | tidak | |
| `labels` | array string, maks 20 | tidak | Ditambahkan ke label yang sudah ada. Label baru dibuat otomatis |
| `fields` | object | tidak | Nilai custom field kontak, dengan kunci field sebagai nama properti |
| `owner_email` | string | tidak | Email anggota workspace yang menjadi pemilik |
| `acquisition_source` | string | tidak | Hanya dipakai saat kontak **baru** dibuat. Salah satu dari `organic`, `ctwa_ad`, `tracking_link`, `campaign`, `import`, `manual`, `api`, `referral` |

```bash
curl -X POST https://pesanvo.com/api/v1/contacts/upsert \
  -H "X-Api-Key: $KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "081234567890",
    "name": "Budi Santoso",
    "labels": ["Website"],
    "fields": { "kota": "Bandung" },
    "acquisition_source": "api"
  }'
```

Balasan `201` bila kontak baru dibuat, `200` bila diperbarui. Isi `data` sama dengan [Detail kontak](#detail-kontak), ditambah `"created": true|false`.

## Detail kontak

`GET /api/v1/contacts/{phone}` · scope `crm:read`

```bash
curl https://pesanvo.com/api/v1/contacts/081234567890 -H "X-Api-Key: $KEY"
```

```json
{
  "success": true,
  "data": {
    "id": "01K2CONT...",
    "name": "Budi Santoso",
    "phone": "6281234567890",
    "email": null,
    "owner_id": 7,
    "lifecycle_stage": "prospect",
    "acquisition_source": "api",
    "labels": ["Website"],
    "fields": { "kota": "Bandung" },
    "open_deals": [ ]
  }
}
```

`open_deals` berisi hingga 20 deal terbuka milik kontak ini, dengan bentuk yang sama seperti [deal](#bentuk-data-deal).

---

# Deal

## Buat deal

`POST /api/v1/deals` · scope `crm:write`

| Parameter | Tipe | Wajib | Keterangan |
|---|---|---|---|
| `phone` | string | salah satu | Nomor kontak. Kontak dibuat otomatis bila belum ada |
| `contact_id` | string | salah satu | ID kontak yang sudah ada |
| `title` | string, maks 120 | ya | Judul deal |
| `value` | integer | tidak | Nilai dalam rupiah. Bawaan `0` |
| `pipeline_id` | string | tidak | Kosongkan untuk pipeline bawaan |
| `stage_id` | integer | tidak | Kosongkan untuk tahap pertama |
| `owner_email` | string | tidak | Email anggota workspace yang menjadi pemilik |
| `fields` | object | tidak | Nilai custom field deal |
| `external_id` | string, maks 100 | tidak | ID pesanan di sistem Anda. Lihat di bawah |

```bash
curl -X POST https://pesanvo.com/api/v1/deals \
  -H "X-Api-Key: $KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "081234567890",
    "title": "Order #INV-2041",
    "value": 1250000,
    "external_id": "INV-2041"
  }'
```

### Mencegah deal ganda dengan `external_id`

Bila `external_id` yang sama dikirim lagi, deal yang sudah ada dikembalikan dengan `"created": false` dan tidak ada deal baru yang dibuat. Kirim selalu `external_id` bila permintaan Anda bisa terulang, misalnya karena timeout atau antrean yang mengulang job.

## Perbarui deal

`PATCH /api/v1/deals/{id}` · scope `crm:write`

| Parameter | Tipe | Keterangan |
|---|---|---|
| `title` | string | |
| `value` | integer | Ditolak `422` bila nilai deal dihitung dari produk |
| `owner_email` | string atau `null` | `null` melepas pemilik |
| `fields` | object | Nilai custom field deal |

Kirim hanya kolom yang ingin diubah.

## Pindah tahap

`POST /api/v1/deals/{id}/stage` · scope `crm:write`

| Parameter | Tipe | Wajib | Keterangan |
|---|---|---|---|
| `stage_id` | integer | ya | Tahap tujuan, dari [Daftar pipeline](#daftar-pipeline) |
| `lost_reason` | string | tidak | Dipakai bila tahap tujuan bertipe `lost` |

## Tandai Berhasil

`POST /api/v1/deals/{id}/won` · scope `crm:write`

Tanpa parameter. Custom field wajib tidak menghalangi penutupan lewat API.

## Tandai Gagal

`POST /api/v1/deals/{id}/lost` · scope `crm:write`

| Parameter | Tipe | Keterangan |
|---|---|---|
| `lost_reason` | string, maks 60 | Dicocokkan dengan **nama** alasan gagal di Pengaturan CRM. Bila tidak cocok, dipakai alasan "Lainnya" |
| `note` | string, maks 500 | Catatan tambahan |

## Bentuk data deal

Semua endpoint deal mengembalikan bentuk yang sama:

```json
{
  "success": true,
  "data": {
    "id": "01K2DEAL...",
    "title": "Order #INV-2041",
    "value": 1250000,
    "value_from_items": false,
    "pipeline_id": "01K2PIPE...",
    "stage_id": 11,
    "stage": { "id": 11, "name": "Prospek", "type": "open" },
    "status": "open",
    "contact": { "id": "01K2CONT...", "name": "Budi Santoso", "phone": "6281234567890" },
    "owner_id": 7,
    "next_task": null,
    "is_rotting": false,
    "days_in_stage": 0,
    "external_id": "INV-2041",
    "fields": { },
    "created": true
  }
}
```

`status` bernilai `open`, `won`, atau `lost`. `created` hanya ada di balasan [Buat deal](#buat-deal).

---

# Tugas

## Buat tugas

`POST /api/v1/tasks` · scope `crm:write`

| Parameter | Tipe | Wajib | Keterangan |
|---|---|---|---|
| `deal_id` | string | salah satu | Tugas untuk deal ini |
| `phone` | string | salah satu | Tugas untuk kontak ini. Kontaknya harus sudah ada |
| `title` | string, maks 200 | ya | |
| `due_at` | tanggal ISO 8601 | ya | Jatuh tempo |
| `type` | string | tidak | `follow_up` (bawaan), `call`, `meeting`, `send_quote`, `other` |
| `owner_email` | string | tidak | Email anggota yang mengerjakan |

```bash
curl -X POST https://pesanvo.com/api/v1/tasks \
  -H "X-Api-Key: $KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "081234567890",
    "title": "Telepon konfirmasi alamat pengiriman",
    "due_at": "2026-11-20T10:00:00+07:00",
    "type": "call"
  }'
```

Balasan `201` dengan data tugas.

---

# Menerima perubahan

API CRM dipakai untuk **mengirim** data ke Pesanvo. Untuk **mendengar** perubahan yang dibuat tim Anda, misalnya deal berhasil atau tagihan lunas, daftarkan [Webhook](WEBHOOK.md) dan pilih kejadian CRM seperti `deal.won` atau `invoice.paid`.

## Contoh alur: toko online

1. Pembeli checkout di website → `POST /api/v1/contacts/upsert` lalu `POST /api/v1/deals` dengan `external_id` nomor pesanan.
2. Sales menindaklanjuti di Inbox dan mengirim tagihan dari [Kasir](KASIR_DI_CHAT.md).
3. Pembeli membayar → webhook `invoice.paid` dan `deal.won` sampai di server Anda → pesanan diproses gudang.
