# Webhook

A **Webhook** allows your application to listen for real-time loyalty events on the Gamopanda platform (such as streak milestone completions, challenge failures, or active streak warning periods). When a registered event occurs, Gamopanda sends an HTTP `POST` payload containing the event data to your configured endpoint URL.

:::tip
All webhook notifications are signed using a secure key. You should verify the signature header on your server using the unique `webhookSecret` to ensure the authenticity of incoming requests.
:::

---

## How it works

1. **Configure Webhook** — define a name, endpoint URL, and select the system `eventCodes` you wish to subscribe to.
2. **Retrieve Secret** — once created, the platform autogenerates a unique `webhookSecret` for security verification.
3. **Listen for Events** — when a member executes an action that triggers a subscribed event (e.g. finishes a challenge), the platform compiles the event payload.
4. **Receive Payload** — Gamopanda sends an HTTP `POST` request to your `endpointUrl` with the signature header.
5. **Acknowledge delivery** — your server must respond with a `2xx` HTTP status code. If delivery fails (non-2xx response or timeout), Gamopanda logs the attempt in [Webhook Logs](/webhook-log) and retries automatically.

---

## Fields

### Basic information

| Field | Type | Required | Max length | Description |
|---|---|---|---|---|
| `name` | string | ✅ | 256 | Internal name for the webhook subscription |
| `description` | string | ❌ | 1 024 | Internal description of the webhook |
| `status` | enum | ✅ | — | Subscription status: `draft` · `live` · `paused`. Default `draft`. |

### Endpoint settings

| Field | Type | Required | Max length | Description |
|---|---|---|---|---|
| `endpointUrl` | string | ✅ | 2 048 | The destination URL where HTTP POST requests are sent. Must be a valid URI. |
| `eventCodes` | array (string) | ✅ | — | List of event codes that trigger this webhook. See [Event Codes](#event-codes) below. |

### Read-only settings

| Field | Type | Description |
|---|---|---|
| `webhookSecret` | string | Secure autogenerated token (max 64 chars) used to verify webhook request signatures. |

---

## Event codes

You can subscribe your webhook to one or more of the following event codes:

- `streak_progress_broken` — Sent when a member's streak resets due to inactivity
- `streak_progress_to_be_broken` — Sent as a warning reminder before a streak expires
- `streak_progress_completed` — Sent when a member completes the overall target streak
- `streak_progress_milestone_completed` — Sent when a member crosses a reward milestone in a streak
- `streak_progress_non_milestone_completed` — Sent when a member completes a streak period that doesn't have a milestone
- `streak_progress_in_progress` — Sent when a member makes progress on a streak
- `challenge_progress_failed` — Sent when a challenge window closes without completion
- `challenge_progress_to_be_failed` — Sent as a warning reminder before a challenge expires
- `challenge_progress_completed` — Sent when a member completes the overall challenge
- `challenge_progress_milestone_completed` — Sent when a member reaches a milestone in a challenge
- `challenge_progress_in_progress` — Sent when a member makes progress on a challenge

---

## Webhook signatures

Each webhook request contains a signature header (`x-gamopanda-signature`) containing the HMAC SHA-256 hash of the request body, signed using your webhook's unique `webhookSecret`.

To verify a webhook request:
1. Retrieve the signature from the `x-gamopanda-signature` header.
2. Compute the HMAC SHA-256 hash of the raw HTTP request body using the `webhookSecret` as the signing key.
3. Compare the computed hash with the header signature value. To prevent timing attacks, perform a constant-time comparison.

---

### Implementation

### Node.js

```js
import crypto from 'crypto';

function verifyWebhookSignature(rawBody, signature, secret) {
  const computedSignature = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');

  const computedBuffer = Buffer.from(computedSignature, 'hex');
  const signatureBuffer = Buffer.from(signature, 'hex');

  if (computedBuffer.length !== signatureBuffer.length) {
    return false;
  }

  // Prevent timing attacks
  return crypto.timingSafeEqual(computedBuffer, signatureBuffer);
}
```

---

### Python

```python
import hmac
import hashlib

def verify_webhook_signature(raw_body: bytes, signature: str, secret: str) -> bool:
    computed_signature = hmac.new(
        secret.encode('utf-8'),
        raw_body,
        hashlib.sha256
    ).hexdigest()
    
    # Prevent timing attacks
    return hmac.compare_digest(computed_signature, signature)
```

---

### Java

```java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.nio.charset.StandardCharsets;

public class WebhookVerifier {
    public static boolean verifySignature(byte[] rawBody, String signature, String secret) {
        try {
            Mac sha256HMAC = Mac.getInstance("HmacSHA256");
            SecretKeySpec secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
            sha256HMAC.init(secretKey);
            byte[] hashBytes = sha256HMAC.doFinal(rawBody);
            
            StringBuilder hexString = new StringBuilder();
            for (byte b : hashBytes) {
                String hex = Integer.toHexString(0xff & b);
                if (hex.length() == 1) hexString.append('0');
                hexString.append(hex);
            }
            String computedSignature = hexString.toString();
            
            // Prevent timing attacks
            return MessageDigest.isEqual(
                computedSignature.getBytes(StandardCharsets.UTF_8),
                signature.getBytes(StandardCharsets.UTF_8)
            );
        } catch (Exception e) {
            return false;
        }
    }
}
```

---

### PHP

```php
function verifyWebhookSignature(string $rawBody, string $signature, string $secret): bool {
    $computedSignature = hash_hmac('sha256', $rawBody, $secret);
    
    // Prevent timing attacks
    return hash_equals($computedSignature, $signature);
}
```

---

### Ruby

```ruby
require 'openssl'
require 'rack'

def verify_webhook_signature(raw_body, signature, secret)
  computed_signature = OpenSSL::HMAC.hexdigest(
    OpenSSL::Digest.new('sha256'),
    secret,
    raw_body
  )
  
  # Prevent timing attacks
  Rack::Utils.secure_compare(computed_signature, signature)
end
```

---

## Real-world examples

**🛍️ E-commerce — Streak Milestone Webhook Subscription**

Subscribe to streak progress events to trigger immediate actions on your backend (e.g., updating user CRM fields or badges):

```json
{
  "name": "Streak Milestones Webhook",
  "description": "Triggered when a member crosses streak milestones.",
  "status": "live",
  "endpointUrl": "https://api.my-shop.com/webhooks/gamopanda",
  "eventCodes": [
    "streak_progress_milestone_completed"
  ]
}
```

---

## Access & permissions

| Caller | Allowed operations | Notes |
|---|---|---|
| Admin | CREATE · GET · LIST · UPDATE · DELETE | Full control over webhook subscriptions |
| End user | *(none)* | No access |
| Guest user | *(none)* | No access |

---

## Related resources

| Resource | Description |
|---|---|
| [Webhook Log](/webhook-log) | Read-only delivery logs recording execution status and retries |
| [Event Log](/event-log) | The source system audit events that triggered the webhook |

---

## API reference

See the [API Reference](/api/webhook) for full request/response schemas and interactive examples for:

- `GET /schema/webhook/record` — list webhooks
- `POST /schema/webhook/record` — create a webhook
- `GET /schema/webhook/record/{id}` — get a webhook by ID
- `PATCH /schema/webhook/record/{id}` — update a webhook
- `DELETE /schema/webhook/record/{id}` — delete a webhook
