# Autentikasi & Penandatanganan Permintaan

> Tandatangani permintaan API dengan HMAC-SHA256 menggunakan project UUID dan API key Anda.

Setiap permintaan API (kecuali webhook masuk) harus membawa project UUID dan tanda tangan permintaan Anda. Tanda tangan membuktikan bahwa permintaan berasal dari Anda dan tidak ada yang mengubahnya selama pengiriman.

## API key

2328.io menggunakan **dua key** yang berbagi algoritma penandatanganan yang sama tetapi mencakup endpoint yang berbeda:

| Key | Digunakan untuk |
|-----|----------|
| **API key** | Pembayaran, dompet statis, saldo, nilai tukar, dan verifikasi webhook pembayaran / dompet statis |
| **Payout API key** | Semua endpoint `/v1/payout/*` dan verifikasi webhook penarikan |

Kedua key tersedia di pengaturan project Anda di [2328.io](https://2328.io). Contoh di bawah menyebut "API key" secara umum — gantikan dengan key yang sesuai untuk endpoint yang Anda panggil.

> **INFO:** **Jangan pernah** mencampur kedua key: menandatangani permintaan penarikan dengan API key biasa (atau permintaan pembayaran dengan payout key) akan mengembalikan kesalahan tanda tangan.

## Header yang diperlukan

| Header | Tipe | Wajib | Deskripsi |
|--------|------|----------|-------------|
| `Content-Type` | string | ya | Selalu `application/json` |
| `project` | string | ya | Project UUID Anda |
| `sign` | string | ya | Tanda tangan HMAC-SHA256 dari permintaan, dihitung dengan API key Anda |
| `User-Agent` | string | ya | Mengidentifikasi aplikasi Anda (mis. `MyShop/1.4 (+https://myshop.example)`). Permintaan tanpa `User-Agent` dapat diblokir. |

## Cara kerja tanda tangan

Anggap tanda tangan sebagai sidik jari dari body permintaan. Dibuat dengan:

1. Serialisasi body ke JSON (kompak — tanpa spasi tambahan).
2. Encode JSON tersebut ke Base64. Langkah ini menormalkan input lintas bahasa — setelah menjadi ASCII murni, setiap bahasa menghasilkan byte yang sama untuk HMAC.
3. Hitung **HMAC-SHA256** dari string Base64 menggunakan API key Anda, lalu konversi hasilnya ke huruf kecil hex.

Untuk **GET** dan tipe permintaan lain tanpa body, tandatangani string kosong sebagai pengganti JSON.

> **INFO:** Tanda tangan string kosong bersifat konstan untuk API key tertentu. Anda dapat menyimpannya di cache jika melakukan banyak panggilan GET.

## Implementasi

#### php

```php
<?php
function apiSign(array $data, string $apiKey): string {
    $json = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    $base64 = base64_encode($json);
    return hash_hmac('sha256', $base64, $apiKey);
}
```

#### js

```js
import crypto from "crypto";

export function apiSign(data, apiKey) {
  const json = JSON.stringify(data);
  const base64 = Buffer.from(json).toString("base64");
  return crypto.createHmac("sha256", apiKey).update(base64).digest("hex");
}
```

#### ts

```ts
import { createHmac } from "crypto";

export function apiSign(data: object, apiKey: string): string {
  const json = JSON.stringify(data);
  const base64 = Buffer.from(json).toString("base64");
  return createHmac("sha256", apiKey).update(base64).digest("hex");
}
```

#### python

```python
import json
import hmac
import hashlib
import base64

def api_sign(data: dict, api_key: str) -> str:
    # ensure_ascii=False keeps non-ASCII characters (Cyrillic, Chinese, …)
    # as-is. Without it, Python escapes them to \uXXXX and the signature
    # diverges from PHP / Node / Go.
    body = json.dumps(data, separators=(",", ":"), ensure_ascii=False)
    b64 = base64.b64encode(body.encode("utf-8")).decode()
    return hmac.new(api_key.encode(), b64.encode(), hashlib.sha256).hexdigest()
```

#### go

```go
package sign

import (
    "bytes"
    "crypto/hmac"
    "crypto/sha256"
    "encoding/base64"
    "encoding/hex"
    "encoding/json"
)

func ApiSign(data any, apiKey string) (string, error) {
    // json.Encoder with SetEscapeHTML(false) — without it, Go escapes <, >, &
    // to \u003c etc., which breaks compatibility with PHP / Node / Python.
    var buf bytes.Buffer
    enc := json.NewEncoder(&buf)
    enc.SetEscapeHTML(false)
    if err := enc.Encode(data); err != nil {
        return "", err
    }
    // Encoder appends a trailing newline — drop it.
    body := bytes.TrimRight(buf.Bytes(), "\n")

    b64 := base64.StdEncoding.EncodeToString(body)
    h := hmac.New(sha256.New, []byte(apiKey))
    h.Write([]byte(b64))
    return hex.EncodeToString(h.Sum(nil)), nil
}
```

### Permintaan tanpa body (GET)

Untuk permintaan tanpa body (mis. `GET /v1/payout/status/{uuid}`), tandatangani string kosong:

#### curl

```curl
SIGN=$(printf '' | openssl dgst -sha256 -hmac "$API_KEY" -hex | awk '{print $NF}')
```

#### php

```php
$sign = hash_hmac('sha256', base64_encode(''), $apiKey);
```

#### js

```js
import { createHmac } from "crypto";

const sign = createHmac("sha256", apiKey)
  .update(Buffer.from("").toString("base64"))
  .digest("hex");
```

#### ts

```ts
import { createHmac } from "crypto";

const sign: string = createHmac("sha256", apiKey)
  .update(Buffer.from("").toString("base64"))
  .digest("hex");
```

#### python

```python
import hmac
import hashlib
import base64

sign = hmac.new(
    api_key.encode(),
    base64.b64encode(b"").decode().encode(),
    hashlib.sha256,
).hexdigest()
```

#### go

```go
package sign

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/base64"
    "encoding/hex"
)

func EmptyBodySign(apiKey string) string {
    b64 := base64.StdEncoding.EncodeToString([]byte(""))
    h := hmac.New(sha256.New, []byte(apiKey))
    h.Write([]byte(b64))
    return hex.EncodeToString(h.Sum(nil))
}
```

## Contoh permintaan lengkap

#### curl

```curl
curl -X POST https://api.2328.io/api/v1/payment \
  -H "Content-Type: application/json" \
  -H "User-Agent: MyShop/1.0 (+https://myshop.example)" \
  -H "project: YOUR_PROJECT_UUID" \
  -H "sign: YOUR_HMAC_SIGNATURE" \
  -d '{"amount":"100.00","currency":"USD","order_id":"ORDER-123"}'
```

#### php

```php
<?php
function apiSign(string $body, string $apiKey): string {
    return hash_hmac('sha256', base64_encode($body), $apiKey);
}

$project = 'YOUR_PROJECT_UUID';
$apiKey  = 'YOUR_API_KEY';

$data = [
    'amount'   => '100.00',
    'currency' => 'USD',
    'order_id' => 'ORDER-123',
];

$body = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$sign = apiSign($body, $apiKey);

$ch = curl_init('https://api.2328.io/api/v1/payment');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => $body,
    CURLOPT_HTTPHEADER => [
        'Content-Type: application/json',
        'User-Agent: MyShop/1.0 (+https://myshop.example)',
        "project: $project",
        "sign: $sign",
    ],
]);

$response = json_decode(curl_exec($ch), true);
```

#### js

```js
import { createHmac } from "crypto";

function apiSign(body, apiKey) {
  const base64 = Buffer.from(body, "utf8").toString("base64");
  return createHmac("sha256", apiKey).update(base64).digest("hex");
}

const data = {
  amount: "100.00",
  currency: "USD",
  order_id: "ORDER-123",
};

const body = JSON.stringify(data);
const sign = apiSign(body, process.env.API_KEY);

const res = await fetch("https://api.2328.io/api/v1/payment", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "User-Agent": "MyShop/1.0 (+https://myshop.example)",
    project: process.env.PROJECT_UUID,
    sign,
  },
  body,
});

const json = await res.json();
```

#### ts

```ts
import { createHmac } from "crypto";

function apiSign(body: string, apiKey: string): string {
  const base64 = Buffer.from(body, "utf8").toString("base64");
  return createHmac("sha256", apiKey).update(base64).digest("hex");
}

type CreatePaymentBody = {
  amount: string;
  currency: string;
  order_id: string;
};

type CreatePaymentResponse = { state: number; result: unknown };

const data: CreatePaymentBody = {
  amount: "100.00",
  currency: "USD",
  order_id: "ORDER-123",
};

const body = JSON.stringify(data);
const sign = apiSign(body, process.env.API_KEY!);

const res = await fetch("https://api.2328.io/api/v1/payment", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "User-Agent": "MyShop/1.0 (+https://myshop.example)",
    project: process.env.PROJECT_UUID!,
    sign,
  },
  body,
});

const json = (await res.json()) as CreatePaymentResponse;
```

#### python

```python
import json
import hmac
import hashlib
import base64
import httpx

def api_sign(body: str, api_key: str) -> str:
    b64 = base64.b64encode(body.encode("utf-8")).decode()
    return hmac.new(api_key.encode(), b64.encode(), hashlib.sha256).hexdigest()

data = {
    "amount": "100.00",
    "currency": "USD",
    "order_id": "ORDER-123",
}

body = json.dumps(data, separators=(",", ":"), ensure_ascii=False)
sign = api_sign(body, API_KEY)

r = httpx.post(
    "https://api.2328.io/api/v1/payment",
    headers={
        "Content-Type": "application/json",
        "User-Agent": "MyShop/1.0 (+https://myshop.example)",
        "project": PROJECT_UUID,
        "sign": sign,
    },
    content=body.encode("utf-8"),
)
response = r.json()
```

#### go

```go
package main

import (
    "bytes"
    "crypto/hmac"
    "crypto/sha256"
    "encoding/base64"
    "encoding/hex"
    "encoding/json"
    "net/http"
)

func ApiSign(body []byte, apiKey string) string {
    b64 := base64.StdEncoding.EncodeToString(body)
    h := hmac.New(sha256.New, []byte(apiKey))
    h.Write([]byte(b64))
    return hex.EncodeToString(h.Sum(nil))
}

func marshalCanonical(v any) ([]byte, error) {
    var buf bytes.Buffer
    enc := json.NewEncoder(&buf)
    enc.SetEscapeHTML(false)
    if err := enc.Encode(v); err != nil {
        return nil, err
    }
    return bytes.TrimRight(buf.Bytes(), "\n"), nil
}

func main() {
    data := struct {
        Amount   string `json:"amount"`
        Currency string `json:"currency"`
        OrderID  string `json:"order_id"`
    }{
        Amount:   "100.00",
        Currency: "USD",
        OrderID:  "ORDER-123",
    }

    body, err := marshalCanonical(data)
    if err != nil {
        panic(err)
    }
    sign := ApiSign(body, apiKey)

    req, _ := http.NewRequest("POST",
        "https://api.2328.io/api/v1/payment",
        bytes.NewReader(body))
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("User-Agent", "MyShop/1.0 (+https://myshop.example)")
    req.Header.Set("project", projectUUID)
    req.Header.Set("sign", sign)

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()
}
```

> **DANGER:** **Jangan pernah mengekspos API key Anda di kode sisi klien.** Tandatangani permintaan di backend Anda. API key yang bocor memberi siapa pun akses penuh ke akun merchant Anda.

## Memverifikasi tanda tangan webhook

Saat 2328.io mengirim webhook kepada Anda, algoritma yang sama berjalan secara terbalik:

1. Tarik field `sign` dari payload.
2. Encode field yang tersisa ke JSON (kompak, tanpa spasi).
3. Encode string tersebut ke Base64.
4. Hitung `HMAC-SHA256` dengan key yang sesuai.
5. Bandingkan dengan `sign` yang diterima menggunakan perbandingan **constant-time** (`hash_equals`, `crypto.timingSafeEqual`, `hmac.compare_digest`, `subtle.ConstantTimeCompare`, `OpenSSL.fixed_length_secure_compare`).

Key penandatanganan tergantung pada sumber webhook:

| Webhook | Key untuk verifikasi |
|---------|---------------------|
| Webhook pembayaran / dompet statis (`/v1/payment`, `/v1/static-wallet`) | **API key** |
| Webhook penarikan (`/v1/payout`) | **Payout API key** |

> **WARNING:** **Kesalahan umum verifikasi.** Encoder JSON Anda harus menghasilkan **byte yang persis sama** dengan yang dihasilkan pengirim — jika tidak, Base64 akan berbeda dan tanda tangan tidak akan cocok.
> 
> - **Go**: gunakan `json.NewEncoder` dengan `SetEscapeHTML(false)`. `json.Marshal` default melakukan escape `<`, `>`, `&` menjadi `<` dan merusak tanda tangan.
> - **Python**: berikan `ensure_ascii=False` ke `json.dumps`. Tanpa itu, karakter non-ASCII (Sirilik, Tionghoa, …) di-escape menjadi `\uXXXX`.
> - **JSON ringkas**: tanpa spasi antar field (`separators=(",", ":")` di Python).
> - **Urutan field** (Go): `map[string]any` biasa mengacak kunci saat di-encode ulang. Gunakan `json.RawMessage`, struct yang berurutan, atau hapus `sign` dari byte mentah.
> 
> Jika verifikasi terus gagal, jalankan `apiSign` pada payload Anda sendiri — hasilnya harus berupa string heksadesimal yang sama dengan `sign` yang diterima.

> **INFO:** **Tanda tangan yang valid tidak mencegah replay.** Tanda tangan hanya membuktikan bahwa webhook berasal dari 2328.io — bukan menghentikan penyerang untuk mengirim ulang webhook yang *telah ditangkap* belakangan. Selalu periksa idempotensi berdasarkan `uuid` (atau `txid` untuk dompet statis) sebelum mengkreditkan dana. Tolak dengan HTTP `401` jika tanda tangan hilang atau salah.

Contoh kode lengkap ada di **[Webhook Notifications](/docs/webhooks#verifying-the-signature)**. Penanganan retry dan aturan idempotensi ada di [Praktik terbaik](/docs/webhooks#best-practices).