# Webhooks

Receive real-time notifications when payment events occur. Webhooks allow your application to react instantly to completed payments without polling.

***

## Overview

```
┌─────────────────┐         ┌─────────────────┐         ┌─────────────────┐
│    Customer     │         │    MakaPay      │         │   Your Server   │
│                 │         │                 │         │                 │
│  Sends payment  │ ──────→ │  Processes      │         │                 │
│                 │         │  payment        │         │                 │
│                 │         │       ↓         │         │                 │
│                 │         │  Sends webhook  │ ──────→ │  Receives POST  │
│                 │         │                 │         │  request        │
│                 │         │                 │         │       ↓         │
│                 │         │                 │         │  Fulfills order │
└─────────────────┘         └─────────────────┘         └─────────────────┘
```

***

## Setup

### Prerequisites

* An active API key (required for webhook authentication)
* An HTTPS endpoint on your server

### Option A: Configure via Dashboard

1. Go to **Dashboard → Webhooks**
2. Enter your webhook URL (must be HTTPS)
3. Toggle **Active** to enable
4. Click **Save**

### Option B: Configure via API

You can manage webhooks programmatically using the Payment Webhook Configuration API:

```bash
# Create/update webhook
curl -X PUT https://app.makapay.io/api/v1/webhooks \
  -H "x-api-key: mk_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/webhook",
    "events": ["payment.completed", "payment.withdrawn"]
  }'

# Get current configuration
curl -X GET https://app.makapay.io/api/v1/webhooks \
  -H "x-api-key: mk_your_api_key"

# Regenerate signing secret
curl -X POST https://app.makapay.io/api/v1/webhooks \
  -H "x-api-key: mk_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"action": "regenerate-secret"}'

# Delete webhook
curl -X DELETE https://app.makapay.io/api/v1/webhooks \
  -H "x-api-key: mk_your_api_key"
```

See the [API Reference](/developers/api-reference.md#payment-webhook-configuration-api) for full details.

### Webhook Signing Secret

When you create a webhook via the API, a dedicated signing secret (`whsec_...`) is generated. This secret is used to sign webhook payloads instead of your API key, providing better security isolation.

* The full secret is only returned on creation and regeneration
* Subsequent reads show a masked preview: `whsec_************************abcd1234`
* Use `POST /api/v1/webhooks` with `{"action": "regenerate-secret"}` to rotate the secret
* Existing webhooks created via the dashboard continue using the API key for signing (backward compatible)

### Event Filtering

Subscribe only to the events you need:

* `payment.completed` — Payment confirmed (default)
* `payment.withdrawn` — Funds sent to merchant wallet (default)
* `payment.awaiting_gas` — Gas tank needs top-up
* `payment.failed` — Payment failed
* `payment.expired` — Payment expired

If no events are specified, defaults to `["payment.completed", "payment.withdrawn"]`.

### Generate API Key

If you don't have an API key:

1. Go to **Dashboard → API Keys**
2. Click **Create New Key**
3. Save the key securely

***

## Webhook Events

### `payment.completed`

Sent when a payment is successfully settled to your wallet.

```json
{
  "event": "payment.completed",
  "paymentId": "01234567-89ab-cdef-0123-456789abcdef",
  "orderId": "order-12345",
  "merchant": "0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00",
  "recipientAddress": "0xabc123...",
  "amount": "100.00",
  "token": "0xc2132D05D31c914a87C6611C10748AEb04B58e8F",
  "chainId": 137,
  "description": "Premium subscription",
  "status": "completed",
  "transactionHash": "0x...",
  "overpayment": "0.00",
  "feeDetails": {
    "gasUsed": 145000,
    "gasPrice": "30000000000",
    "usdtAmountUsed": "1.13",
    "merchantType": "direct"
  },
  "terminal": {
    "terminalId": "term_aZ3kLm9NqP",
    "code": "R1",
    "name": "Register 1"
  },
  "timestamp": 1704067200000
}
```

> **`terminal`** is set when the payment originated from a POS terminal (the `orderId` follows `POS-{CODE}-{CASHIER}-{TIMESTAMP}`). It's `null` for regular payment-link payments and for POS payments whose terminal record has been deleted. `terminalId` is the stable identifier you should store; `code` is the short value embedded in `orderId`; `name` is the dashboard display name. Same shape as the `terminal` field on `GET /v1/payments`.

### `payment.withdrawn`

Sent when a partial payment is manually withdrawn.

```json
{
  "event": "payment.withdrawn",
  "paymentId": "01234567-89ab-cdef-0123-456789abcdef",
  "orderId": "order-12345",
  "merchant": "0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00",
  "recipientAddress": "0xabc123...",
  "amount": "100.00",
  "amountWithdrawn": "75.00",
  "token": "0xc2132D05D31c914a87C6611C10748AEb04B58e8F",
  "chainId": 137,
  "status": "withdrawn",
  "withdrawalTransactionHash": "0x...",
  "terminal": null,
  "timestamp": 1704067200000
}
```

### `payment.awaiting_gas`

Sent when payment is received but Gas Tank balance is insufficient. **Informational only** - no retry on this event.

```json
{
  "event": "payment.awaiting_gas",
  "paymentId": "01234567-89ab-cdef-0123-456789abcdef",
  "orderId": "order-12345",
  "merchant": "0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00",
  "amount": "100.00",
  "status": "awaiting_gas",
  "estimatedFeeUsdt": "1.13",
  "gasTankBalance": "0.50",
  "requiredTopUp": "0.63",
  "terminal": null,
  "timestamp": 1704067200000
}
```

***

## Webhook Security

### Signature Verification

Every webhook includes a signature header for verification:

```
X-Signature: a1b2c3d4e5f6...
```

The signature is computed as `SHA-256(payload + signingKey)` — a SHA-256 hash of the raw JSON body concatenated with your signing key.

**Signing key priority:**

* If you created/updated your webhook via the API and received a `whsec_...` secret, that secret is used as the signing key.
* For legacy webhooks configured via the dashboard (before dedicated secrets were introduced), the API key is used as the signing key.

### Verifying the Signature (Node.js)

```javascript
const crypto = require('crypto');

function verifyWebhook(rawBody, signature, signingKey) {
  // SHA-256 of payload + signing key
  const expected = crypto
    .createHash('sha256')
    .update(rawBody + signingKey)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(expected, 'hex'),
    Buffer.from(signature, 'hex')
  );
}

// In your webhook handler
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-signature'];
  const rawBody = req.body.toString();

  // Use your whsec_... secret, or API key for legacy webhooks
  const isValid = verifyWebhook(rawBody, signature, YOUR_WEBHOOK_SECRET);

  if (!isValid) {
    return res.status(401).send('Invalid signature');
  }

  const payload = JSON.parse(rawBody);
  // Process webhook...
  res.status(200).send('OK');
});
```

### Python Example

```python
import hashlib

def verify_webhook(raw_body: bytes, signature: str, signing_key: str) -> bool:
    # SHA-256 of payload + signing key
    expected = hashlib.sha256(
        raw_body + signing_key.encode()
    ).hexdigest()
    return expected == signature
```

***

## Retry Policy

Failed webhook deliveries are automatically retried:

| Attempt | Delay      |
| ------- | ---------- |
| 1st     | Immediate  |
| 2nd     | 5 minutes  |
| 3rd     | 10 minutes |

### What Counts as Failure

* HTTP status code outside 200-299
* Connection timeout (30 seconds)
* SSL/TLS errors
* DNS resolution failure

### After All Retries Fail

* Webhook is marked as failed
* No further automatic retries
* You can manually retry from the dashboard

***

## Best Practices

### 1. Respond Quickly

Return a 200 response as fast as possible. Do heavy processing asynchronously.

```javascript
app.post('/webhook', async (req, res) => {
  // Respond immediately
  res.status(200).send('OK');

  // Process asynchronously
  processPayment(req.body).catch(console.error);
});
```

### 2. Implement Idempotency

Webhooks may be delivered more than once. Use `paymentId` to prevent duplicate processing.

```javascript
async function processPayment(webhook) {
  const exists = await db.payments.findOne({
    paymentId: webhook.paymentId
  });

  if (exists) {
    console.log('Already processed, skipping');
    return;
  }

  // Process payment...
  await db.payments.insert({ paymentId: webhook.paymentId, ... });
}
```

### 3. Verify Signatures

Always verify the `X-Signature` header before processing.

### 4. Use HTTPS

Webhook URLs must use HTTPS. HTTP endpoints are rejected.

### 5. Handle All Event Types

Your endpoint should gracefully handle unknown event types:

```javascript
switch (webhook.event) {
  case 'payment.completed':
    handleCompleted(webhook);
    break;
  case 'payment.withdrawn':
    handleWithdrawn(webhook);
    break;
  default:
    console.log('Unknown event type:', webhook.event);
}
```

***

## Testing Webhooks

### Manual Trigger

1. Go to **Dashboard → Payments**
2. Click on a completed payment
3. Click **Resend Webhook**
4. Check your webhook logs

### Webhook Logs

View delivery history in **Dashboard → Webhooks → Logs**:

| Column      | Description                 |
| ----------- | --------------------------- |
| Payment ID  | Associated payment          |
| Status      | success / failed / pending  |
| Status Code | HTTP response code          |
| Attempts    | Delivery attempts made      |
| Response    | Server response (truncated) |

### Local Testing

Use a tunnel service like ngrok for local development:

```bash
ngrok http 3000
# Use the https URL as your webhook endpoint
```

***

## Payload Reference

### Common Fields

| Field              | Type   | Description               |
| ------------------ | ------ | ------------------------- |
| `event`            | string | Event type                |
| `paymentId`        | string | Unique payment identifier |
| `orderId`          | string | Your order reference      |
| `merchant`         | string | Merchant wallet address   |
| `recipientAddress` | string | Payment address           |
| `amount`           | string | Payment amount            |
| `token`            | string | Token contract address    |
| `chainId`          | number | Blockchain network ID     |
| `status`           | string | Payment status            |
| `timestamp`        | number | Unix timestamp (ms)       |

### Optional Fields

| Field                       | Type   | When Present              |
| --------------------------- | ------ | ------------------------- |
| `description`               | string | If provided at creation   |
| `transactionHash`           | string | On completed payments     |
| `withdrawalTransactionHash` | string | On withdrawals            |
| `amountWithdrawn`           | string | On partial withdrawals    |
| `overpayment`               | string | If customer overpaid      |
| `feeDetails`                | object | On completed payments     |
| `blockConfirmations`        | number | If confirmations required |

***

## Troubleshooting

### Webhook Not Received

1. Check webhook is **Active** in dashboard
2. Verify URL is correct and uses HTTPS
3. Check your server logs for incoming requests
4. Review webhook logs for delivery status

### Signature Mismatch

1. Ensure you're using the correct API key
2. Verify you're hashing the raw JSON payload
3. Check for encoding issues (UTF-8)

### Timeouts

1. Respond with 200 before processing
2. Move heavy operations to background jobs
3. Increase server timeout if needed

### Duplicate Events

1. Implement idempotency using `paymentId`
2. Check webhook logs for retry attempts
3. Review your response codes (non-2xx triggers retry)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.makapay.io/developers/webhooks.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
