# Payment Webhook Handler

Every time there is a successful payment for your store, Teya makes a call to the webhook URLs created for that store.

The webhook request includes an `x-teya-signature` header that must be verified to ensure the call was made by Teya.

The public key for the signature was returned at the webhook creation from the [Teya Business Portal](https://business.teya.com/settings/integrations). The public key associated to a specific webhook can be retrieved at any time from the Teya Business Portal.

We use the SHA256withRSA algorithm.

For the signature verification, you should use the entire payload received in the call made from Teya.

## Webhook Payload Fields

| Field | Description | Example |
|-------|-------------|---------|
| `event` | Event type name | `payment.succeeded.v1` |
| `timestamp` | Time the event occurred (ISO 8601) | `2025-10-01T08:36:06Z` |
| `data.transaction_id` | Unique identifier for the transaction, which can be used for refunds and receipt creation. | `tr_01999eea-03f5-78ae-962b-97ff946aad4d` |
| `data.authentication_id` | Unique identifier for the authentication | `ee8125ef-501f-4051-be2f-6714565aa0f1` |
| `data.payment_method.type` | Payment method type | `CARD` |
| `data.payment_method.brand` | Card brand | `MASTERCARD` |
| `data.payment_method.last4` | Last 4 digits of card number | `0000` |
| `data.amount` | Payment amount, same as used in the session, payment link or transaction creation | `{"value": 50, "currency": "GBP"}` |
| `data.status` | Payment status, we only send for successful payments currently | `SUCCESS` |
| `data.session_id` | The payment session identifier, if applicable, can be used to identify the order. | `01999ee9-c5d6-791f-ba20-4156434915f6` |
| `data.payment_link_id` | The payment link identifier, if applicable. | `20eb276c-5b6b-4474-a5a1-49bcd6193148` |
| `data.authorisation_code` | Card authorisation code | `471274` |
| `data.merchant_reference` | Reference used when creating the session. | `my-order-123` |
| `data.token_response` | Optional. Contains details of the token created for the card. `null` when no token was created for the payment. `token_type` is `CARD_TOKEN` for a token created from card details and `WALLET_TOKEN` for one created from Apple Pay; send it back when charging the token. | `{"token_id": "5f7b1a3a-3b7a-4b1a-9b1a-3a3b7a4b1a9b", "token_status": "SUCCESS", "token_type": "CARD_TOKEN"}` |

## Example Payload

### Headers

```
user-agent: Teya-Webhooks
x-teya-signature: gOzDXhS7M8afuYjYx9EGTjOUXraexOijxN7V9f+hlourPwrZ6e6nrrs6mRlFb5xLyfCDIOneKUORdNNX9tFMPdTruPwLnjE6OhZnv9IKSJZ4B1C0aewED2koC4taO9Ye9sHqmK3ZRSP9c31CG2KCV3+mGa92UeDqfWtc+w6R8z4mr98yuvi76nHFjdTG59UIRgAjItr9vwo7HLbshMH0xe1MTmuaDx3D5pDJqmQAE0/kMptpbURwLU6oegaaX0CZ/4MB5A5Wq+PgdDxUxJ6A5N1s1bqtn7hs9nIgSfSvTCIKpOXjZeMQtw+8zT9CEEJ7ASwMAEk0LlN7v+2thutMqA==

```

### Body

```json
{
  "event": "payment.succeeded.v1",
  "timestamp": "2025-12-02T12:04:22Z",
  "data": {
    "transaction_id": "tr_019adef3-0d07-7543-9b74-efa2d5b6fa2a",
    "payment_method": {
      "type": "CARD",
      "brand": "MASTERCARD",
      "last4": "4065"
    },
    "amount": {
      "value": 3434,
      "currency": "GBP"
    },
    "status": "SUCCESS",
    "session_id": "019adef2-e5b3-7e00-9fe1-61a5a01f3eef",
    "payment_link_id": "019adef2-e00a-75f2-8d6d-490b345981b2",
    "authentication_id": "019adef3-0cc3-7645-9d87-5cd0357e8509",
    "authorisation_code": "776761",
    "merchant_reference": "asdasd",
    "token_response": null
  }
}
```

```admonition
icon: fas fa-tools
title: Test your signature validation
content: Use the <a href="/partners/online-payments/webhook-signature-validator">Webhook Signature Validator</a> tool to verify your implementation is correctly validating webhook signatures before going live.
```

## Validate Signatures

Every webhook sent by Teya includes an `x-teya-signature` header containing a cryptographic signature. Validating this signature ensures:

- **Authenticity**: The webhook was genuinely sent by Teya
- **Integrity**: The payload has not been modified in transit
- **Security**: Protection against replay attacks and forged requests

## Signature Algorithm

Teya uses **SHA256withRSA** (RSASSA-PKCS1-v1_5 with SHA-256) for webhook signatures. The signature is Base64 encoded.

## Validation Process

### Step 1: Extract the Signature

Extract the `x-teya-signature` header from the incoming webhook request:

```
x-teya-signature: gOzDXhS7M8afuYjYx9EGTjOUXra...
```

### Step 2: Get the Raw Request Body

Use the **entire raw request body** exactly as received. Do not parse, format, or modify the JSON in any way.

> **Important**: Use the raw bytes of the request body, not a parsed and re-serialized JSON object. Any modification to whitespace or field ordering will cause validation to fail.

### Step 3: Retrieve Your Public Key

The public key for your webhook was provided when you created the webhook in the Teya Business Portal. You can retrieve it at any time from the portal under your webhook configuration.

### Step 4: Verify the Signature

Decode the Base64 signature and verify it against the raw payload using your public key.

## Delivery and Retries

Teya sends each event as an HTTP `POST` request to your webhook URL. Your endpoint must reply with a `2xx` status code within **30 seconds** to confirm it received the event. Teya also gives up on connecting to your server after **5 seconds**.

### When a delivery fails

Teya treats a delivery as failed when:

- Your endpoint returns any status code that is not `2xx` (for example `4xx` or `5xx`)
- Your endpoint does not reply within 30 seconds
- Teya cannot connect to your server (for example DNS failure, connection refused or TLS error)

### Retry schedule

When a delivery fails, Teya retries it with increasing delays. Each event is attempted up to **6 times** in total:

| Attempt | Delay after previous attempt | Time after event |
|---------|------------------------------|------------------|
| 1st | Immediately | 00:00 |
| 2nd | 5 minutes | 00:05 |
| 3rd | 15 minutes | 00:20 |
| 4th | 1 hour | 01:20 |
| 5th | 2 hours | 03:20 |
| 6th | 6 hours | 09:20 |

If the 6th attempt also fails, Teya stops retrying and the event is not delivered. If you deactivate a webhook, any pending retries for it are dropped.

```admonition
icon: fas fa-info-circle
title: Handle duplicate events
content: A retry can reach you even when you processed the event but your reply did not arrive in time (for example your server timed out after saving the payment). Use <code>data.transaction_id</code> to detect and ignore events you have already processed.
```

### Tips for reliable delivery

- **Reply first, process later**: return `2xx` as soon as you have stored the event, then do any slow work (sending emails, updating stock) asynchronously
- **Do not reply `2xx` when you could not store the event**: return a `5xx` instead so Teya retries it
- **Keep your endpoint idempotent**: processing the same event twice must not create duplicate orders or refunds

## Troubleshooting

### Common Issues

| Issue | Cause | Solution |
|-------|-------|----------|
| Signature always fails | Modified payload | Use the raw request body, not parsed JSON |
| Invalid key format | Wrong key format | Ensure key is in PEM format with correct headers |
| Encoding errors | Character encoding | Use UTF-8 encoding consistently |
| Missing signature header | Framework stripping headers | Check your web framework's header handling |

### Debugging Tips

1. **Log the raw payload**: Print the exact bytes received to verify no modification occurred
2. **Check header case**: Some frameworks lowercase headers (`x-teya-signature` vs `X-Teya-Signature`)
3. **Verify key format**: Ensure newlines are preserved in the PEM key

## Security Best Practices

- **Always validate** signatures before processing webhook data
- **Store keys securely** using environment variables or a secrets manager
- **Return quickly** with a `2xx` response to avoid webhook retries (see [Delivery and Retries](#delivery-and-retries)), then process asynchronously
- **Log validation failures** for security monitoring
