# API LAZISNU — Mitra Eksternal

Spesifikasi integrasi API untuk **mitra eksternal** pada platform **NU Care / CMS LAZISNU**.

> **Status:** Endpoint `/api/v1/partners/*` telah diimplementasikan. Dokumen ini menjadi kontrak integrasi resmi untuk mitra eksternal.

---

## 1. Ringkasan

| Aspek | Keterangan |
|---|---|
| Base path | `/api/v1/partners/*` |
| Identitas mitra | API Key + `X-Partner-Code` (kode unik per mitra) |
| Format body create | `application/json` |
| Response envelope | `{ meta, data, pagination? }` |
| Create donasi/zakat/kurban | 1 endpoint unified `POST /transactions` |
| List program | filter `category_id` / `sub_category_id` |
| Auth | API Key + HMAC signature (recommended) |

---

## 2. Base URL & Versioning

Semua endpoint menggunakan path prefix:

```
/api/v1/partners
```

| Environment | Keterangan |
|---|---|
| Production | Host production diberikan saat onboarding integrasi |
| Staging | Host staging diberikan saat onboarding integrasi |

Format lengkap: `https://{host}/api/v1/partners/{endpoint}`

> Hostname (base URL) tidak dipublikasikan di dokumentasi publik. Hubungi tim integrasi LAZISNU untuk mendapatkan endpoint environment Anda.

Semua endpoint **wajib** menggunakan prefix versi (`/v1/`). Perubahan breaking hanya melalui versi baru (`/v2/`).

---

## 3. Autentikasi & Keamanan

### 3.1 Header wajib

| Header | Wajib | Contoh | Keterangan |
|---|---|---|---|
| `Authorization` | Ya | `Bearer {api_key}` | API key mitra (diberikan LAZISNU) |
| `X-Partner-Code` | Ya | `your_partner_code` | Kode mitra unik (snake_case, diberikan saat onboarding) |
| `Content-Type` | Ya (POST/PUT) | `application/json` | |
| `Accept` | Disarankan | `application/json` | |
| `X-Request-Id` | Disarankan | UUID v4 | Trace request |
| `Idempotency-Key` | Ya (POST transaksi) | UUID v4 | Cegah duplikasi transaksi |
| `X-Timestamp` | Ya (HMAC mode) | `1718000000` | Unix timestamp (detik) |
| `X-Signature` | Ya (HMAC mode) | `sha256=...` | HMAC-SHA256 body |

### 3.2 Partner code

`X-Partner-Code` berisi kode unik mitra dalam format **snake_case**. Nilai kode diberikan secara khusus oleh tim LAZISNU saat onboarding dan bersifat **rahasia**.

**Contoh:**

| Field | Contoh |
|---|---|
| Header `X-Partner-Code` | `your_partner_code` |
| Response `partner_code` | `your_partner_code` |
| Response `partner_name` | `YOUR PARTNER` |

> Daftar lengkap partner code tidak dipublikasikan. Hubungi tim integrasi LAZISNU untuk mendapatkan kode mitra Anda.

### 3.3 HMAC Signature (recommended)

```
signature = HMAC_SHA256(api_secret, timestamp + "." + raw_json_body)
Header: X-Signature: sha256={signature}
```

- Timestamp toleransi: **±300 detik**
- Request di luar window → `401 INVALID_SIGNATURE`

### 3.4 Rate limit

| Tier | Limit |
|---|---|
| Default | 60 request / menit / API key |
| Burst create transaction | 10 request / detik |

Response headers:
- `X-RateLimit-Limit`
- `X-RateLimit-Remaining`
- `Retry-After` (saat `429`)

---

## 4. Format Response Standar

### 4.1 Success (single resource)

```json
{
  "meta": {
    "code": 200,
    "status": "success",
    "message": null,
    "response_date": "2026-06-03T10:12:07+07:00",
    "request_id": "550e8400-e29b-41d4-a716-446655440000"
  },
  "data": {}
}
```

### 4.2 Success (list + pagination)

```json
{
  "meta": {
    "code": 200,
    "status": "success",
    "message": null,
    "response_date": "2026-06-03T10:12:07+07:00",
    "request_id": "550e8400-e29b-41d4-a716-446655440000"
  },
  "data": [],
  "pagination": {
    "current_page": 1,
    "per_page": 20,
    "total": 150,
    "last_page": 8
  }
}
```

### 4.3 Error

```json
{
  "meta": {
    "code": 422,
    "status": "error",
    "message": "Validation failed",
    "response_date": "2026-06-03T10:12:07+07:00",
    "request_id": "550e8400-e29b-41d4-a716-446655440000"
  },
  "data": null,
  "errors": {
    "amount": ["Minimal nominal transaksi Rp 5.000"]
  }
}
```

### 4.4 HTTP status mapping

| HTTP | `meta.code` | Keterangan |
|---|---|---|
| 200 | 200 | OK |
| 201 | 201 | Created |
| 400 | 400 | Bad request |
| 401 | 401 | Unauthorized / invalid signature |
| 403 | 403 | Partner tidak punya akses resource |
| 404 | 404 | Resource tidak ditemukan |
| 409 | 409 | Idempotency conflict |
| 422 | 422 | Validation error |
| 429 | 429 | Rate limit |
| 500 | 500 | Internal server error |

---

## 5. Data Model (referensi `app/Models`)

### 5.1 Transaction (`transactions`)

| Field | Type | Required | Keterangan |
|---|---|---|---|
| `id` | integer | auto | Primary key internal |
| `trx_id` | string | auto | Format: `LZNU{4digit}{timestamp}` |
| `amount` | integer | ya | Nominal Rupiah (tanpa desimal) |
| `name` | string | ya | Nama donatur/muzakki |
| `email` | string | opsional | |
| `phone` | string | opsional | Nomor telepon donatur |
| `address` | string | opsional | |
| `description` | string | opsional | Catatan transaksi |
| `donation_id` | integer | ya | ID program donasi/zakat/qurban |
| `category_id` | integer | auto | Dari relasi `donation.category_id` |
| `organization_id` | integer | auto | Dari relasi `donation.organization_id` |
| `status` | enum | auto | `PENDING`, `SUCCESS`, `EXPIRED` |
| `via_method` | enum | auto | Selalu `partner` untuk mitra eksternal |
| `payment_method` | string | ya | Di-set server dari `X-Partner-Code` (contoh: `your_partner_code`) |
| `is_anon` | boolean | opsional | Default `false` |
| `is_published` | boolean | opsional | Default `true` |
| `meta_data` | object | opsional | Data tambahan bebas (JSON) |
| `partner_reference_id` | string | opsional | ID transaksi di sisi mitra (disarankan) |
| `expired_at` | datetime | auto | ISO8601 WIB |
| `created_at` | datetime | auto | ISO8601 WIB |
| `updated_at` | datetime | auto | ISO8601 WIB |

### 5.2 Donation / Campaign (`donations`)

| Field | Type | Keterangan |
|---|---|---|
| `id` | integer | ID program |
| `title` | string | Judul campaign |
| `slug` | string | URL slug |
| `descriptions` | string | Deskripsi HTML/text |
| `target` | decimal | Target nominal |
| `total_collected` | integer | Agregat transaksi SUCCESS |
| `category_id` | integer | Lihat tabel kategori |
| `sub_category_id` | integer | Sub-kategori program |
| `organization_id` | integer | LAZISNU pengelola |
| `status` | enum | `processed`, `approved`, `rejected` |
| `is_active` | boolean | Hanya `true` yang ditampilkan ke mitra |
| `started_at` | datetime | |
| `ended_at` | datetime | |

### 5.3 DonationCategory (`donation_categories`)

| ID | Kode | Nama |
|---|---|---|
| 1 | ZN | Zakat |
| 2 | DN | Donasi |
| 3 | CN | Campaign / Crowdfunding |
| 4 | QN | Qurban |
| 5 | FN | Fidyah |
| 6 | KN | Koin NU |
| 7 | WK | Wakaf |

### 5.4 Organization (`organizations`)

| Field | Type | Keterangan |
|---|---|---|
| `id` | integer | |
| `name` | string | |
| `slug` | string | |
| `code` | string | Kode unit LAZISNU |
| `phone` | string | Nomor telepon |
| `email` | string | |
| `address` | string | Alamat lengkap |
| `npwp` | string | |
| `is_active` | boolean | |

---

## 6. Endpoint

### 6.1 Profil Mitra

**GET** `/me`

**Response 200**

```json
{
  "meta": { "code": 200, "status": "success", "message": null, "response_date": "2026-06-03T10:12:07+07:00", "request_id": "..." },
  "data": {
    "partner_code": "your_partner_code",
    "partner_name": "YOUR PARTNER",
    "organization": {
      "id": 1,
      "name": "LAZISNU Pusat",
      "slug": "lazisnu-pusat",
      "phone": "021-xxxx",
      "email": "info@lazisnu.or.id",
      "address": "Jakarta",
      "npwp": "xx.xxx.xxx.x-xxx.xxx"
    },
    "allowed_category_ids": [1, 2, 3, 4],
    "webhook_url": "https://partner.example.com/webhooks/lazisnu"
  }
}
```

---

### 6.2 Kategori & Sub-kategori

**GET** `/categories`

**Response 200:** list kategori aktif.

**GET** `/categories/{category_id}/sub-categories`

Mengembalikan daftar sub-kategori untuk kategori tertentu.

---

### 6.3 Campaign / Donasi (Crowdfunding)

**GET** `/campaigns`

| Query | Type | Keterangan |
|---|---|---|
| `category_id` | integer | Default `3` (Campaign) |
| `sub_category_id` | integer | Filter sub-kategori |
| `organization_id` | integer | Filter LAZISNU unit |
| `search` | string | Cari judul |
| `status` | string | Default hanya `approved` + `is_active=true` |
| `page` | integer | Default `1` |
| `per_page` | integer | Default `20`, max `100` |

**Response 200 (item)**

```json
{
  "id": 9,
  "title": "Bantu Pulihkan Pesantren Terdampak Banjir Sultra",
  "slug": "bantu_pulihkan_pesantren_terdampak_banjir_sultra",
  "category": { "id": 3, "name": "Campaign", "slug": "campaign" },
  "sub_category": { "id": 6, "name": "Pendidikan", "slug": "pendidikan" },
  "status": "open",
  "target": 500000000,
  "total_collected": 125000000,
  "organization": { "id": 1, "name": "LAZISNU Pusat", "slug": "lazisnu-pusat" },
  "started_at": "2026-01-01 00:00:00",
  "ended_at": "2026-12-31 23:59:59"
}
```

**GET** `/campaigns/{id}` — detail campaign.

**GET** `/campaigns/slug/{slug}` — detail by slug.

---

### 6.4 Zakat

**GET** `/zakat`

| Query | Type | Keterangan |
|---|---|---|
| `type` | string | `zakat_maal`, `zakat_profesi`, `zakat_fitrah`, `zakat` |
| `page`, `per_page` | integer | Pagination |

**Response 200 (item)**

```json
{
  "id": 58,
  "title": "Zakat Maal",
  "slug": "zakat_maal",
  "program_type": "zakat",
  "category_id": 1,
  "status": "open",
  "minimum_amount": 10000
}
```

---

### 6.5 Qurban

**GET** `/qurban/products`

| Query | Type | Keterangan |
|---|---|---|
| `organization_id` | integer | Opsional |
| `page`, `per_page` | integer | Pagination |

**Response 200 (item)**

```json
{
  "id": 37,
  "title": "Kambing",
  "slug": "kambing",
  "category_id": 4,
  "price": 2900000,
  "status": "open",
  "organization": { "id": 1, "name": "LAZISNU Pusat" }
}
```

---

### 6.6 Transaksi (Unified Create)

**POST** `/transactions`

**Headers tambahan:** `Idempotency-Key: {uuid}`

**Request body**

```json
{
  "donation_id": 58,
  "amount": 1000000,
  "name": "Andi Chan",
  "email": "andichan@gmail.com",
  "phone": "0853981253678",
  "npwp": "1234567891234567",
  "address": "Jakarta",
  "description": "Donasi via mitra",
  "is_anon": false,
  "partner_reference_id": "PTR-20260603-001",
  "meta_data": {
    "channel": "partner_checkout",
    "order_id": "1234567890"
  }
}
```

| Field | Required | Validasi |
|---|---|---|
| `donation_id` | ya | exists, active, whitelisted untuk partner |
| `amount` | ya | integer, min `5000` (qurban: harus match `price` jika fixed) |
| `name` | ya | max 255 |
| `email` | opsional | valid email |
| `phone` | opsional | 8–15 digit |
| `npwp` | opsional | 15–16 digit |
| `partner_reference_id` | disarankan | unique per partner |
| `meta_data` | opsional | object |

> `payment_method` **tidak** dikirim client — di-set server dari `X-Partner-Code`.  
> `via_method` otomatis `partner`.

**Response 201**

```json
{
  "meta": { "code": 201, "status": "success", "message": "Transaction created", "response_date": "...", "request_id": "..." },
  "data": {
    "id": 4,
    "trx_id": "LZNU00011718000000",
    "donation_id": 58,
    "donation": {
      "id": 58,
      "title": "Zakat Maal",
      "slug": "zakat_maal"
    },
    "amount": 1000000,
    "name": "Andi Chan",
    "email": "andichan@gmail.com",
    "phone": "0853981253678",
    "status": "SUCCESS",
    "via_method": "partner",
    "payment_method": "your_partner_code",
    "partner_code": "your_partner_code",
    "partner_reference_id": "PTR-20260603-001",
    "created_at": "2026-06-03T10:12:07+07:00"
  }
}
```

> Untuk mitra eksternal, transaksi biasanya langsung `SUCCESS` (pembayaran sudah terjadi di platform mitra). Jika flow async, status awal `PENDING` + webhook follow-up.

---

### 6.7 Query Transaksi

**GET** `/transactions`

| Query | Type | Keterangan |
|---|---|---|
| `status` | string | `PENDING`, `SUCCESS`, `EXPIRED` |
| `donation_id` | integer | |
| `partner_reference_id` | string | |
| `trx_id` | string | |
| `date_from`, `date_to` | date | `Y-m-d` |
| `page`, `per_page` | integer | |

**GET** `/transactions/{trx_id}`

Detail transaksi by `trx_id`.

**GET** `/transactions/history`

Shortcut: hanya `status=SUCCESS`, sorted `created_at desc`.

---

## 7. Webhook ke Mitra (Outbound)

LAZISNU dapat mengirim notifikasi ke URL mitra saat status transaksi berubah.

**POST** `{partner_webhook_url}`

**Headers dari LAZISNU**

```
Content-Type: application/json
X-Lazisnu-Signature: sha256={hmac}
X-Lazisnu-Timestamp: 1718000000
X-Request-Id: uuid
```

**Payload**

```json
{
  "event": "transaction.status_updated",
  "data": {
    "trx_id": "LZNU00011718000000",
    "partner_reference_id": "SHP-20260603-001",
    "status": "SUCCESS",
    "amount": 1000000,
    "payment_method": "your_partner_code",
    "paid_at": "2026-06-03T10:15:00+07:00",
    "donation": {
      "id": 58,
      "title": "Zakat Maal",
      "slug": "zakat_maal"
    }
  }
}
```

**Expected response mitra:** HTTP `200` dengan body `{ "received": true }`

Retry policy LAZISNU: 3x exponential backoff (1m, 5m, 30m).

---

## 8. Idempotency

- Header `Idempotency-Key` wajib untuk `POST /transactions`
- Key yang sama + payload identik → return transaksi existing (`200`, bukan `201`)
- Key sama + payload berbeda → `409 Conflict`

---

## 9. Best Practices Integrasi

1. **Simpan `trx_id` dan `partner_reference_id`** di database mitra untuk reconciliasi.
2. **Gunakan webhook + polling** (`GET /transactions/{trx_id}`) sebagai fallback.
3. **Validasi amount server-side** — jangan percaya nominal dari client tanpa verifikasi di mitra.
4. **Whitelist campaign** — mitra hanya bisa create transaksi untuk program yang diizinkan.
5. **Log `X-Request-Id`** di kedua sisi untuk support/debugging.
6. **Rotate API key** berkala; dukung 2 key aktif saat rotasi.
7. **Sandbox** — uji di staging sebelum production.
8. **Timezone** — semua datetime ISO8601 dengan offset `+07:00` (WIB).

---

## 10. Lampiran

- OpenAPI 3.0: [`openapi-mitra-eksternal.yaml`](./openapi-mitra-eksternal.yaml)
- Postman Collection: [`API LAZISNU - Mitra Eksternal.postman_collection.json`](./API%20LAZISNU%20-%20Mitra%20Eksternal.postman_collection.json)

---

## 11. Changelog

| Versi | Tanggal | Perubahan |
|---|---|---|
| 1.0.0 | 2026-06-03 | Initial spec mitra eksternal |
