Metadata-Version: 2.4
Name: unuspay-sdk
Version: 0.1.1
Summary: Official UnusPay SDK for webhook signature verification
Project-URL: Homepage, https://github.com/unuspay/unuspay-sdk
Project-URL: Documentation, https://docs.unuspay.com
Author: UnusPay
License-Expression: MIT
Keywords: payments,signature,unuspay,verification,webhook
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Requires-Python: >=3.9
Requires-Dist: pydantic>=2.0.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0.0; extra == 'dev'
Description-Content-Type: text/markdown

# unuspay-sdk

Official UnusPay SDK for webhook signature verification.

## Installation

```bash
pip install unuspay-sdk
```

## Usage

### Flask

```python
from flask import Flask, request, jsonify
from unuspay_sdk import Webhook, WebhookVerificationError
import os

app = Flask(__name__)
webhook = Webhook(secret=os.environ['WEBHOOK_SECRET'])

@app.route('/webhook', methods=['POST'])
def webhook_handler():
    try:
        event = webhook.verify(
            payload=request.data,  # Raw bytes, NOT request.json
            signature=request.headers.get('X-Webhook-Signature'),
            timestamp=request.headers.get('X-Webhook-Timestamp'),
        )

        # Handle the event
        if event['type'] == 'order.completed':
            print(f"Order completed: {event['data']}")
        elif event['type'] == 'payment_link.created':
            print(f"Payment link created: {event['data']}")

        return jsonify({'received': True})

    except WebhookVerificationError as e:
        print(f"Webhook verification failed: {e}")
        return jsonify({'error': str(e)}), 401
```

### FastAPI

```python
from fastapi import FastAPI, Request, HTTPException
from unuspay_sdk import Webhook, WebhookVerificationError
import os

app = FastAPI()
webhook = Webhook(secret=os.environ['WEBHOOK_SECRET'])

@app.post('/webhook')
async def webhook(request: Request):
    try:
        payload = await request.body()  # Raw bytes
        
        event = webhook.verify(
            payload=payload,
            signature=request.headers.get('X-Webhook-Signature'),
            timestamp=request.headers.get('X-Webhook-Timestamp'),
        )

        print(f"Received event: {event['type']}")
        return {'received': True}

    except WebhookVerificationError as e:
        raise HTTPException(status_code=401, detail=str(e))
```

### Django

```python
# views.py
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from unuspay_sdk import Webhook, WebhookVerificationError
import os

webhook = Webhook(secret=os.environ['WEBHOOK_SECRET'])

@csrf_exempt
@require_POST
def webhook_handler(request):
    try:
        event = webhook.verify(
            payload=request.body,  # Raw bytes
            signature=request.headers.get('X-Webhook-Signature'),
            timestamp=request.headers.get('X-Webhook-Timestamp'),
        )

        print(f"Received event: {event['type']}")
        return JsonResponse({'received': True})

    except WebhookVerificationError as e:
        return JsonResponse({'error': str(e)}, status=401)
```

## API Reference

### `Webhook` class

Main class for webhook signature verification.

#### Constructor

```python
Webhook(secret: str, config: Optional[WebhookConfig] = None)
```

**Parameters:**

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `secret` | `str` | Yes | Your webhook secret (starts with `whsec_`) |
| `config` | `WebhookConfig` | No | Optional configuration (see below) |

#### `verify` method

Verifies a webhook signature and returns the parsed payload.

```python
webhook.verify(payload: Union[str, bytes], signature: str, timestamp: Union[str, int]) -> Any
```

**Parameters:**

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `payload` | `str` or `bytes` | Yes | Raw request body (NOT parsed JSON) |
| `signature` | `str` | Yes | Value of `X-Webhook-Signature` header |
| `timestamp` | `str` or `int` | Yes | Value of `X-Webhook-Timestamp` header |

**Returns:** Parsed event dictionary

**Raises:** `WebhookVerificationError` if verification fails

### `WebhookConfig` class

Optional configuration for webhook verification.

```python
WebhookConfig(max_age_seconds: int = 300)
```

**Parameters:**

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `max_age_seconds` | `int` | 300 | Max event age in seconds |

### `WebhookVerificationError`

Exception raised when verification fails.

**Attributes:**
- `message`: Human-readable error message
- `code`: Error code (`'INVALID_SIGNATURE'` | `'TIMESTAMP_EXPIRED'` | `'MISSING_HEADERS'`)

## Important: Raw Body Required

Webhook signature verification requires the **raw request body** (exact bytes as received). Do NOT use parsed JSON.

**Common gotchas:**

- **Flask:** Use `request.data`, NOT `request.json`
- **FastAPI:** Use `await request.body()`, NOT `await request.json()`
- **Django:** Use `request.body`, NOT `json.loads(request.body)`

## License

MIT
