# Authentication और Request Signing

> अपने project UUID और API key का उपयोग करके HMAC-SHA256 के साथ API request को sign करें।

प्रत्येक API request (incoming webhook को छोड़कर) में आपका project UUID और एक request signature होना ज़रूरी है। Signature यह सिद्ध करता है कि request आपकी ओर से आया है और रास्ते में किसी ने उसे बदला नहीं है।

## API keys

2328.io **दो keys** का उपयोग करता है जो एक ही signing algorithm share करती हैं लेकिन अलग-अलग endpoint को cover करती हैं:

| Key | किस लिए उपयोग होती है |
|-----|----------|
| **API key** | भुगतान, स्टैटिक वॉलेट, बैलेंस, विनिमय दर, और payment / static-wallet webhook का verification |
| **Payout API key** | सभी `/v1/payout/*` endpoint और payout webhook का verification |

दोनों keys आपके [2328.io](https://2328.io) project settings में रहती हैं। नीचे के examples में "API key" सामान्य रूप से लिखा है — आप जिस endpoint को call कर रहे हैं, उसके लिए सही key का उपयोग करें।

> **INFO:** दोनों keys को **कभी न मिलाएँ**: सामान्य API key से payout request को sign करना (या payout key से payment request को sign करना) signature error लौटाता है।

## आवश्यक headers

| Header | Type | आवश्यक | Description |
|--------|------|----------|-------------|
| `Content-Type` | string | हाँ | हमेशा `application/json` |
| `project` | string | हाँ | आपका project UUID |
| `sign` | string | हाँ | आपकी API key से compute किया गया request का HMAC-SHA256 सिग्नेचर |
| `User-Agent` | string | हाँ | आपके एप्लिकेशन की पहचान करता है (जैसे `MyShop/1.4 (+https://myshop.example)`). `User-Agent` के बिना अनुरोध ब्लॉक किए जा सकते हैं। |

## Signature कैसे काम करता है

Signature को request body का fingerprint समझें। इसे इस तरह बनाया जाता है:

1. Body को JSON में serialize करें (compact — कोई अतिरिक्त whitespace नहीं)।
2. उस JSON को Base64 में encode करें। यह कदम input को सभी languages में normalise करता है — एक बार जब यह plain ASCII हो जाता है, हर language HMAC के लिए समान bytes produce करती है।
3. अपनी API key का उपयोग करके Base64 string का **HMAC-SHA256** compute करें, फिर परिणाम को lowercase hex में बदलें।

**GET** और बिना body वाले अन्य request types के लिए, JSON के बजाय एक empty string को sign करें।

> **INFO:** किसी दिए गए API key के लिए empty-string सिग्नेचर constant होता है। यदि आप कई GET calls करते हैं तो इसे cache कर सकते हैं।

## Implementations

#### 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
}
```

### बिना body वाले request (GET)

खाली body वाले request के लिए (जैसे `GET /v1/payout/status/{uuid}`), एक empty string को sign करें:

#### 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))
}
```

## पूरा request example

#### 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:** **अपनी API key को client-side code में कभी expose न करें।** Request को अपने backend पर sign करें। एक leak हुई API key किसी को भी आपके merchant account तक पूरी पहुँच दे देती है।

## Webhook signatures को verify करना

जब 2328.io आपको webhook भेजता है, तो वही algorithm उल्टा चलता है:

1. Payload से `sign` field निकालें।
2. बाकी fields को JSON-encode करें (compact, बिना whitespace)।
3. उस string को Base64-encode करें।
4. उपयुक्त key के साथ `HMAC-SHA256` compute करें।
5. इसकी तुलना प्राप्त `sign` से **constant-time** comparison के साथ करें (`hash_equals`, `crypto.timingSafeEqual`, `hmac.compare_digest`, `subtle.ConstantTimeCompare`, `OpenSSL.fixed_length_secure_compare`)।

Signing key webhook source पर निर्भर करती है:

| Webhook | Verify करने की key |
|---------|---------------------|
| Payment / static-wallet webhooks (`/v1/payment`, `/v1/static-wallet`) | **API key** |
| Payout webhooks (`/v1/payout`) | **Payout API key** |

> **WARNING:** **सत्यापन के सामान्य pitfalls.** आपका JSON encoder **बिल्कुल वही bytes** उत्पन्न करना चाहिए जो भेजने वाले ने उत्पन्न किए — अन्यथा Base64 अलग होगा और signature मेल नहीं खाएगी।
> 
> - **Go**: `json.NewEncoder` का उपयोग `SetEscapeHTML(false)` के साथ करें। डिफ़ॉल्ट `json.Marshal`, `<`, `>`, `&` को `<` के रूप में escape करता है और signature तोड़ देता है।
> - **Python**: `json.dumps` को `ensure_ascii=False` पास करें। इसके बिना, गैर-ASCII (Cyrillic, Chinese, …) अक्षर `\uXXXX` के रूप में escape हो जाते हैं।
> - **Compact JSON**: फ़ील्ड्स के बीच कोई whitespace नहीं (Python में `separators=(",", ":")`)।
> - **फ़ील्ड क्रम** (Go): एक सादा `map[string]any` re-encode पर keys को randomise कर देता है। `json.RawMessage`, एक ordered struct का उपयोग करें, या raw bytes से `sign` को हटा दें।
> 
> यदि सत्यापन बार-बार विफल हो रहा है, तो स्वयं payload पर `apiSign` चलाएं — यह प्राप्त `sign` के समान ही hex string उत्पन्न करना चाहिए।

> **INFO:** **एक वैध signature replays को नहीं रोकती।** यह केवल यह साबित करती है कि webhook 2328.io से आया है — यह किसी हमलावर को बाद में *कैप्चर किए गए* webhook को फिर से पोस्ट करने से नहीं रोकती। फंड क्रेडिट करने से पहले हमेशा `uuid` (या static wallets के लिए `txid`) से idempotency जांचें। यदि signature गायब है या गलत है तो HTTP `401` से reject करें।

पूर्ण code examples **[Webhook Notifications](/docs/webhooks#verifying-the-signature)** पर हैं। Retry handling और idempotency नियम [Best practices](/docs/webhooks#best-practices) में हैं।