# Sidigi Payment Gateway — Integration Guide (IT)

**Base URL:** `https://qris.ag1it.fun`  
**Browser docs:** [https://qris.ag1it.fun/docs](https://qris.ag1it.fun/docs)  
**Register (PIN):** [https://qris.ag1it.fun/portal/register.html](https://qris.ag1it.fun/portal/register.html)  
**Merchant portal:** [https://qris.ag1it.fun/portal/](https://qris.ag1it.fun/portal/)  
**Health:** [https://qris.ag1it.fun/health](https://qris.ag1it.fun/health)

**Ketentuan Layanan (Bahasa):** [https://qris.ag1it.fun/ketentuan.html](https://qris.ag1it.fun/ketentuan.html)  
Settlement / pencairan: **2×24 jam (H+2) per transaksi** setelah pembayaran berhasil.

Xendit Create QR Code reference: [Create QR Code](https://archive.developers.xendit.co/api-reference/#create-qr-code)

---

## Callback chain (important)

```text
Merchant app                    Sidigi PG                         Xendit
     |                               |                                |
     |  POST /v1/payments/qris       |                                |
     |  (+ optional callback_url)    |                                |
     |------------------------------>|  POST /qr_codes                |
     |                               |  callback_url =                |
     |                               |  https://qris.ag1it.fun/      |
     |                               |    webhooks/xendit             |
     |                               |------------------------------->|
     |  qr_string (Tag 59 branded)   |  qr_string                     |
     |<------------------------------|<-------------------------------|
     |                               |                                |
     |                               |   payer scans & pays           |
     |                               |                                |
     |                               |  POST qr.payment               |
     |                               |  (x-callback-token)            |
     |                               |<-------------------------------|
     |  POST merchant callback_url   |  verify → mark paid            |
     |  (HMAC signed)                |                                |
     |<------------------------------|                                |
```

1. **Merchant → Sidigi** — create QRIS; optionally pass your `callback_url` (or use the one saved on the merchant profile / portal).
2. **Sidigi → Xendit** — always sets Create QR Code `callback_url` to **our** webhook: `https://qris.ag1it.fun/webhooks/xendit`.
3. **Xendit → Sidigi** — `event: qr.payment` when paid; we verify `x-callback-token`.
4. **Sidigi → Merchant** — after status is `paid`, we POST to **your** callback URL with `X-Sidigi-Signature`.

Merchants never talk to Xendit webhooks directly.

---

## 1. Roles

| Role | How you authenticate |
|------|----------------------|
| **Register (IT / merchant)** | Registration **PIN** on [Register](https://qris.ag1it.fun/portal/register.html) |
| **Admin (Sidigi ops)** | `X-Admin-Key: <ADMIN_API_KEY>` |
| **Merchant (backend)** | `Authorization: Bearer sk_sidigi_...` |
| **Xendit** | POST `/webhooks/xendit` + `x-callback-token` |

### Self-register (PIN)

Open: **https://qris.ag1it.fun/portal/register.html**

Default PIN (change via `REGISTER_PIN` env): ask Sidigi ops (`Sidigi123` on initial deploy).

What register does (**only this**):

1. Validates PIN  
2. Creates merchant (`code`, name, password, **callback_url**)  
3. Returns `api_key` + `callback_secret` once  

It does **not** set bank / disbursement details. Bank verification stays admin-only.

```http
POST https://qris.ag1it.fun/register/merchants
Content-Type: application/json

{
  "pin": "Sidigi123",
  "code": "toko_rafsan",
  "name": "Toko Rafsan",
  "email": "ops@tokorafsan.example",
  "password": "your-portal-password",
  "callback_url": "https://merchant.example.com/webhooks/sidigi"
}
```

### Merchant Portal (browser)

```text
https://qris.ag1it.fun/portal/
```

Login with **merchant code** + portal password.

---

## 2. Admin: create a merchant (optional)

Admin can still create merchants without the PIN page:

```http
POST https://qris.ag1it.fun/admin/merchants
X-Admin-Key: <ADMIN_API_KEY>
Content-Type: application/json

{
  "code": "toko_rafsan",
  "name": "Toko Rafsan",
  "email": "ops@tokorafsan.example",
  "callback_url": "https://merchant.example.com/webhooks/sidigi"
}
```

Response (credentials shown **once**):

```json
{
  "success": true,
  "merchant": {
    "id": "uuid",
    "code": "toko_rafsan",
    "name": "Toko Rafsan",
    "callback_url": "https://merchant.example.com/webhooks/sidigi"
  },
  "credentials": {
    "api_key": "sk_sidigi_...",
    "callback_secret": "whsec_...",
    "portal_password": "..."
  }
}
```

### Update default callback URL

```http
PATCH https://qris.ag1it.fun/admin/merchants/{id}/callback
X-Admin-Key: <ADMIN_API_KEY>
Content-Type: application/json

{ "callback_url": "https://merchant.example.com/hooks/sidigi" }
```

Merchants can also change it in the portal.

### Set / verify bank (once)

```http
PUT https://qris.ag1it.fun/admin/merchants/{id}/bank
X-Admin-Key: <ADMIN_API_KEY>
Content-Type: application/json

{
  "bank_code": "BCA",
  "account_number": "1234567890",
  "account_name": "TOKO RAFSAN",
  "verify": true
}
```

---

## 3. Merchant: create a unique QRIS payment

Uses Xendit **QR Codes V1** (`POST /qr_codes`, type `DYNAMIC`).  
`external_id` must be unique **per merchant**.

```http
POST https://qris.ag1it.fun/v1/payments/qris
Authorization: Bearer sk_sidigi_...
Content-Type: application/json

{
  "external_id": "ORDER-1001",
  "amount": 25000,
  "description": "Order 1001",
  "customer_name": "Budi",
  "callback_url": "https://merchant.example.com/webhooks/sidigi",
  "metadata": { "table": "A3" }
}
```

| Field | Required | Notes |
|-------|----------|--------|
| `external_id` | yes | Your order id |
| `amount` | yes | **Min 1,500** / max 10,000,000 IDR |
| `callback_url` | no | Your webhook for **this** payment; else merchant profile callback |
| `description` / `customer_name` / `metadata` | no | |

Success `201`:

```json
{
  "success": true,
  "payment": {
    "id": "uuid",
    "external_id": "ORDER-1001",
    "amount": 25000,
    "status": "requires_action",
    "merchant_display_name": "Toko Rafsan",
    "callback_url": "https://merchant.example.com/webhooks/sidigi",
    "qr_string": "000201...",
    "expires_at": "...",
    "xendit_qr_id": "qr_..."
  }
}
```

### How to show the QR

1. Render `payment.qr_string` as a QR image  
2. Do **not** alter the EMV string (Tag 59 is already set by Sidigi)  
3. Poll `GET /v1/payments/{id}` **or** wait for your callback  

### Status values

| Status | Meaning |
|--------|---------|
| `pending` / `requires_action` | Waiting for payer |
| `paid` | Confirmed via Xendit `qr.payment` |
| `expired` / `failed` | Unpaid / failed |

---

## 4. Merchant: get payment status

```http
GET https://qris.ag1it.fun/v1/payments/{payment_id}
Authorization: Bearer sk_sidigi_...
```

```http
GET https://qris.ag1it.fun/v1/payments
Authorization: Bearer sk_sidigi_...
```

---

## 5. Merchant callback (you implement this)

When Sidigi verifies Xendit’s `qr.payment`, we POST to **your** URL  
(per-payment `callback_url` or merchant default).

### Headers

| Header | Meaning |
|--------|---------|
| `Content-Type` | `application/json` |
| `X-Sidigi-Signature` | `hex(HMAC-SHA256(callback_secret, raw_body))` |
| `X-Sidigi-Event` | e.g. `payment.paid` |

### Body example

```json
{
  "event": "payment.paid",
  "merchant_code": "toko_rafsan",
  "merchant_name": "Toko Rafsan",
  "payment": {
    "id": "uuid",
    "external_id": "ORDER-1001",
    "amount": 25000,
    "status": "paid",
    "merchant_display_name": "Toko Rafsan",
    "paid_at": "..."
  },
  "sent_at": "..."
}
```

### Verify signature (Node)

```js
import { createHmac, timingSafeEqual } from "crypto";

function verify(rawBody, signatureHeader, secret) {
  const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(signatureHeader || "");
  return a.length === b.length && timingSafeEqual(a, b);
}
```

**Rules**

1. Verify signature before trusting the event  
2. Idempotent on `payment.id` / `external_id`  
3. Respond `2xx` quickly  
4. Fulfill only when `event === "payment.paid"` and `status === "paid"`  
5. Retry: `POST /v1/payments/{id}/retry-callback`

---

## 6. Payouts / disbursement

**Access:** only merchants with **`payout_enabled = true`** (admin-activated) can request disbursement.

```http
POST https://qris.ag1it.fun/admin/merchants/{id}/payout-access
X-Admin-Key: ...
Content-Type: application/json

{ "enabled": true }
```

**Settlement hold:** each paid transaction becomes withdrawable **48 hours (H+2) after its own `paid_at`**.
Until then it shows as `settling` in the balance; `next_release_at` tells you when the oldest one matures.
See [Ketentuan Layanan](https://qris.ag1it.fun/ketentuan.html).

Flow:

1. Admin verifies the merchant bank account once, then activates disbursement (`payout-access`)  
2. Each transaction matures 48h after payment → moves from `settling` to `available`  
3. Merchant requests withdraw manually (`POST /v1/payouts` / portal) **or** enables auto disbursement  
4. Admin marks `approved` → `processing` → `paid` (or `rejected`)  

### Auto disbursement (every 3 days)

Toggle in the portal (**Auto disbursement** card), or:

```http
PATCH https://qris.ag1it.fun/portal/api/auto-payout
Authorization: Bearer <portal session token>
Content-Type: application/json

{ "enabled": true }
```

- Requires a verified bank account  
- First run is 3 days after enabling, then every 3 days  
- Each run requests the **full available balance** as a payout with `source: "auto"`  
- Skipped (schedule still advances) if available is below Rp10.000  
- Payouts still go through the admin status flow below  

```http
POST https://qris.ag1it.fun/v1/payouts
Authorization: Bearer sk_sidigi_...
Content-Type: application/json

{ "amount": 100000, "note": "Weekly settlement" }
```

Admin:

```http
GET https://qris.ag1it.fun/admin/payouts
X-Admin-Key: ...

POST https://qris.ag1it.fun/admin/payouts/{id}/status
X-Admin-Key: ...
Content-Type: application/json

{ "status": "paid", "admin_note": "Disbursed" }
```

Statuses: `requested` → `approved` → `processing` → `paid` | `rejected` | `failed`

---

## 7. Xendit → Sidigi webhook (infra only)

```http
POST https://qris.ag1it.fun/webhooks/xendit
x-callback-token: <XENDIT_CALLBACK_TOKEN>
```

Sidigi sets this URL on every Create QR Code call ([docs](https://archive.developers.xendit.co/api-reference/#create-qr-code)).

Mapped events:

- `qr.payment` / `status: COMPLETED` → payment `paid` → merchant `payment.paid`
- expired / failed → matching merchant callback

Also set the same URL + verification token in Xendit Dashboard Callback Settings.

---

## 8. QR branding (Tag 59 + Tag 60 + Tag 61)

After the QR is created, Sidigi rewrites:

| Tag | Field | Value |
|-----|--------|--------|
| 59 | Merchant name / prefix | Store name from portal or merchant profile (max 25 bytes) |
| 60 | Location / city | **Papua** (`QRIS_CITY`) |
| 61 | Postal code | **99332** (`QRIS_POSTAL_CODE`) |

CRC (Tag 63) is recalculated. Change the store name in the portal; city and zip are fixed.

---

## 9. Error codes

| HTTP | Meaning |
|------|---------|
| 400 | Validation / amount &lt; 1500 / bank not verified |
| 401 | Bad admin or merchant key |
| 403 | Merchant suspended |
| 409 | Duplicate `external_id` |
| 502 | Xendit rejected create |

---

## 10. Merchant IT go-live checklist

- [ ] Registered with PIN **or** received credentials from admin  
- [ ] HTTPS callback with signature verify  
- [ ] Default `callback_url` set (register / portal) **or** pass per QR  
- [ ] Create payment with unique `external_id`  
- [ ] Amount ≥ **1500** IDR  
- [ ] Render `qr_string` without modification  
- [ ] Fulfill only on `payment.paid`  
- [ ] Bank verified **and** disbursement activated by Sidigi ops before payouts  
