# Paytota webhooks (simplified)

Paytota **POST**s JSON to your callback URL when subscribed events fire. The body includes **`event_type`** and **`status`**. Test and live webhooks are separate (same rule as purchases).

**Configure** the URL in the Paytota merchant dashboard (developer / webhooks). For this application use:

`POST {YOUR_APP_BASE}/api/paytota/webhook`

Example: `https://mm.finflo.net/public/api/paytota/webhook`

---

## Signature (`X-Signature`)

Each delivery includes header **`X-Signature`**: base64-encoded **RSA PKCS#1 v1.5** signature of the **SHA-256 digest of the raw request body** (verify the **exact bytes** received, before JSON parsing).

- Obtain the public key from the webhook object in the dashboard, **or** call **`GET {paytota.base_url}/api/v1/public_key/`** with your Bearer secret (same as [paytota.md](paytota.md)).
- Verify with **`openssl_verify($rawBody, base64_decode($xSignature), $pem, OPENSSL_ALGO_SHA256)`** in PHP (see Paytota’s PHP sample in the upstream docs).

**This repo:** [`PaytotaService::verifyWebhookSignature`](app/Libraries/PaytotaService.php) implements verification. Optional **`.env`**:

| Variable | Purpose |
| -------- | ------- |
| `paytota.webhook_public_key` | PEM (certificate or `BEGIN PUBLIC KEY`) pasted from Paytota; avoids fetching `/api/v1/public_key/` on every webhook. |
| `paytota.skip_webhook_signature` | Set to `true` **only in development** to skip verification (unsafe in production). |

If verification is **on** and the signature is missing or invalid, the app responds **403** so Paytota can retry. Invalid JSON or empty body → **400**.

---

## Local behaviour

[`Paytota::webhook`](app/Controllers/Paytota.php) updates **`payment_transactions`** when a row matches:

- **`gateway_reference`** = Paytota **`id`** (purchase or payout UUID), or  
- **`reference`** / **`transaction_id`** = payload **`reference`**

**Purchases** (`product` = `purchases` or `event_type` contains `purchase.`): `paid` → `SUCCESS`; `error`, `failed`, `cancelled`, `void` → `FAILED`; otherwise `PENDING`. Metadata gains **`paytota_webhook`**.

**Payouts** (`payouts` / `payout.`): success-like statuses → `SUCCESS`; error-like → `FAILED`. Metadata gains **`paytota_payout_webhook`**.

If **no** row matches, the webhook is still acknowledged with **200** (log only).

---

## Further reading

- Human-readable API notes: open **`/api/docs`** in the app (Paytota → **POST /webhook**).
- Full upstream detail: Paytota developer documentation (webhooks + signature samples).
