> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pay.plexos.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Receive real-time event notifications

## Overview

Webhooks allow you to receive real-time notifications when events happen in your Plexos Pay account. Instead of polling the API, we push events to your server.

### How It Works

<Steps>
  <Step title="Create a webhook endpoint">
    Register a URL in the dashboard or via the API to receive events.
  </Step>

  <Step title="We send events">
    When something happens (payment debited, refund succeeded, etc.), we send an HTTP POST to your URL.
  </Step>

  <Step title="You verify and process">
    Verify the signature to ensure the event is authentic, then process it.
  </Step>
</Steps>

## Creating a Webhook

```bash theme={null}
curl -X POST https://api.plexospay.com/v1/webhooks \
  -H "Authorization: Bearer sk_test_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://myapp.com/webhooks/plexos",
    "events": ["payment.debited", "payment.failed", "refund.succeeded"],
    "description": "My app webhook"
  }'
```

<Warning>Save the `secret` from the response — it's only shown once and is needed to verify signatures.</Warning>

## Event Types

| Event                  | Description                         |
| ---------------------- | ----------------------------------- |
| `payment.created`      | Payment intent was created          |
| `payment.debited`      | Customer's mobile money was debited |
| `payment.settled`      | Payment was settled to your account |
| `payment.failed`       | Payment failed                      |
| `payment.expired`      | Payment expired before confirmation |
| `payment.cancelled`    | Payment was cancelled               |
| `refund.created`       | Refund was initiated                |
| `refund.succeeded`     | Refund was completed                |
| `refund.failed`        | Refund failed                       |
| `checkout.completed`   | Checkout session was completed      |
| `checkout.expired`     | Checkout session expired            |
| `settlement.completed` | Settlement batch was processed      |

## Event Payload

```json theme={null}
{
  "id": "evt_abc123",
  "type": "payment.debited",
  "data": {
    "id": "pi_def456",
    "amount": "10000",
    "currency": "CVE",
    "status": "DEBITED",
    "operator": "CVMOVEL",
    "operatorPhone": "+2389001234"
  },
  "createdAt": "2025-01-15T10:30:00.000Z"
}
```

## Signature Verification

Every webhook includes two headers for verification:

* `X-Plexos-Signature` — HMAC-SHA256 hex digest
* `X-Plexos-Timestamp` — Unix timestamp (seconds)

The signature is computed as:

```
HMAC-SHA256(secret, "{timestamp}.{body}")
```

### Verification Examples

<Tabs>
  <Tab title="Node.js">
    ```typescript theme={null}
    import { verifyWebhookSignature } from "@plexos-pay/sdk"

    app.post("/webhooks", (req, res) => {
      try {
        const event = verifyWebhookSignature(
          req.body,
          req.headers["x-plexos-signature"],
          req.headers["x-plexos-timestamp"],
          "whsec_your_secret",
        )
        console.log("Event:", event.type)
        res.status(200).send("OK")
      } catch (err) {
        res.status(400).send("Invalid signature")
      }
    })
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    from plexos_pay import verify_webhook_signature

    @app.post("/webhooks")
    def handle_webhook(request):
        try:
            event = verify_webhook_signature(
                body=request.body.decode(),
                signature=request.headers["x-plexos-signature"],
                timestamp=request.headers["x-plexos-timestamp"],
                secret="whsec_your_secret",
            )
            print(f"Event: {event['type']}")
            return {"status": "ok"}
        except ValueError:
            return {"error": "Invalid signature"}, 400
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    import com.plexospay.WebhookVerifier;

    @PostMapping("/webhooks")
    public ResponseEntity<String> handleWebhook(
            @RequestBody String body,
            @RequestHeader("X-Plexos-Signature") String signature,
            @RequestHeader("X-Plexos-Timestamp") String timestamp) {
        try {
            Map<String, Object> event = WebhookVerifier.verify(
                body, signature, timestamp, "whsec_your_secret");
            System.out.println("Event: " + event.get("type"));
            return ResponseEntity.ok("OK");
        } catch (IllegalArgumentException e) {
            return ResponseEntity.badRequest().body("Invalid signature");
        }
    }
    ```
  </Tab>
</Tabs>

## Best Practices

<AccordionGroup>
  <Accordion title="Return 200 quickly">
    Respond with a `200` status code as soon as possible. Process the event asynchronously if needed. We retry on non-2xx responses.
  </Accordion>

  <Accordion title="Handle duplicate events">
    Use the event `id` to deduplicate. We may send the same event more than once.
  </Accordion>

  <Accordion title="Verify signatures">
    Always verify the webhook signature before processing. This prevents attackers from sending fake events.
  </Accordion>

  <Accordion title="Check timestamp freshness">
    Our SDKs automatically reject events older than 5 minutes to prevent replay attacks.
  </Accordion>
</AccordionGroup>
