# Webhooks

Receive and verify HTTP notifications of Support-a-Creator events via webhooks.

Webhooks allow you to receive HTTP notifications of events in the Nexus Support-a-Creator system. Nexus sends each event to an endpoint you expose as an HTTP POST request, containing a JSON payload describing the event.

```json
{
    "eventId": "1_63OfIMngN5aqHEUixc_",
    "createdAt": "2023-02-08 22:56:42.055676",
    "eventType": "Bounty",
    "eventAction": "BountyReward",
    "event": { 
        "attributionGroupId": "B8b84wld-ohveWtgQPJsS", 
        "attributionGroupName": "Great Game",
        "creatorId": "9jd7RBR_B2BZ0uWO3_lgv",
        "bountyId": "tzPlV9PeX9Tsxg158pZ47", 
        "bountyName": "Refer 5 Players", 
        "bountyProgressId": "4a3C8DLDjPToZ_BMRrYXx",  
        "bountyRewards": [
            {
                "id": "prYjeQugJG1n8qFCjW2rM", 
                "name": "In Game Title", 
                "amount": 1, 
                "externalId": "dalfjkAsdlgJkdal", 
                "creatorRewardId": "PDKczbm3bmNFZWcrelC5N",
                "userId": "55d57cffaa074cc6b8d332f5d77661f2"
            }
        ]  
    }
}
```

All notifications will contain `eventId`, `createdAt`, `eventType`, and `eventAction`. The `event` field contains the event's data and will vary based upon the `eventAction` and `eventType` combination.

<LinkCard title="Event type definitions" description="The event payload shapes for each eventType and eventAction." href="/additional-features/webhooks/event-types/" />

## Responding to a notification

Your response to a webhook request must return a `200 OK` status code. If processing of the event will take more than a second or two, consider logging the event and processing it after responding with `200 OK`.

<Aside type="caution">
Failure to respond quickly enough will result in a timeout and subsequent retry of the notification.
</Aside>

## Handling duplicate events

Each event will have an unique `eventId`.

<Aside type="tip">
As duplicate notifications are possible (through retries or, in worst case, replay attacks), we recommend that you log processed `eventId`s so that you can properly ignore any duplicates sent.
</Aside>

## Verifying a notification

<Aside type="tip">
You are not required to verify webhook notifications, but it is highly recommended to prevent replay attacks.
</Aside>

We include a `x-webhook-signature-256` header on each notification request which contains a HMAC signature (signed by your webhook secret) of the event payload, prefixed by `sha256=`.

To verify the notification signature:

1. Create a HMAC signature by concatenating the `eventId`, `createdAt`, and `event` properties from the notification and sign it with your webhook secret.
2. Compare your HMAC to the content of the `x-webhook-signature-256` header.
3. If the signatures match, process the notification. Otherwise, return a `403 Forbidden` response code.

The following javascript snippet shows a simple implementation of this verification process:

```javascript
const express = require('express');
const crypto = require('crypto');
require('dotenv').config();

const app = express();
const port = process.env.PORT || 3099;

// Middleware to parse JSON and capture raw body for signature verification
app.use('/webhook', express.raw({ type: 'application/json' }));
app.use(express.json());

// In-memory store for processed event IDs (in production, use a database)
const processedEventIds = new Set();

// Webhook secret from environment variables
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET || 'your-webhook-secret-here';

/**
 * Create HMAC signature for webhook verification
 */
const getHMac = (eventId, createdAt, event) => {
  // Convert the createdAt string to a Date object, then back to ISO string
  // This ensures we match the server's exact formatting
  const createdAtDate = new Date(createdAt);
  const createdAtISO = createdAtDate.toISOString();
  
  const message = eventId + createdAtISO + JSON.stringify(event);
  
  return crypto.createHmac('sha256', WEBHOOK_SECRET)
    .update(message)
    .digest('hex');
};

/**
 * Verify webhook signature
 */
const verifySignature = (req) => {
  const { eventId, createdAt, event } = JSON.parse(req.body);
  
  const calculatedHmac = getHMac(eventId, createdAt, event);
  
  const signature = req.header('x-webhook-signature-256');
  if (!signature) {
    console.log('No signature header found');
    return false;
  }
  
  const receivedHmac = signature.replace('sha256=', '');
  
  return calculatedHmac === receivedHmac;
};

/**
 * Handle GroupManagement NewMember event
 */
const handleNewMember = (eventData) => {
  console.log('New member joined');
  console.log(`Member ID: ${eventData.memberId}`);
  console.log(`Group Code: ${eventData.code}`);
  console.log(`Group Name: ${eventData.name}`);
  console.log(`Logo Image: ${eventData.logoImage}`);
  
  // Add your business logic here
  // For example: update database, send notifications, etc.
};

/**
 * Handle GroupManagement RemovedMember event
 */
const handleRemovedMember = (eventData) => {
  console.log('Member removed');
  console.log(`Member ID: ${eventData.memberId}`);
  console.log(`Group Code: ${eventData.code}`);
  console.log(`Removal Date: ${eventData.removalDate}`);
  console.log(`Removal Reason: ${eventData.removalReason}`);
  
  // Add your business logic here
  // For example: update database, send notifications, etc.
};

/**
 * Main webhook handler
 */
app.post('/webhook', (req, res) => {
  try {
    // Parse the JSON body
    const payload = JSON.parse(req.body);
    console.log(payload)
    const { eventId, createdAt, eventType, eventAction, event } = payload;
    
    console.log(`Received webhook: ${eventType}:${eventAction} (ID: ${eventId})`);
    
    // Check for duplicate events
    if (processedEventIds.has(eventId)) {
      console.log(`Duplicate event ignored: ${eventId}`);
      return res.status(200).json({ message: 'Event already processed' });
    }
    
    // Verify signature (optional but recommended)
    if (WEBHOOK_SECRET !== 'your-webhook-secret-here' && !verifySignature(req)) {
      console.log('Signature verification failed');
      return res.status(403).json({ error: 'Invalid signature' });
    }
    
    // Process GroupManagement events
    if (eventType === 'GroupManagement') {
      switch (eventAction) {
        case 'NewMember':
          handleNewMember(event);
          break;
        case 'RemovedMember':
          handleRemovedMember(event);
          break;
        default:
          console.log(`Unhandled GroupManagement action: ${eventAction}`);
      }
    } else {
      console.log(`Unhandled event type: ${eventType}`);
    }
    
    // Mark event as processed
    processedEventIds.add(eventId);
    
    // Respond with 200 OK
    res.status(200).json({ 
      message: 'Webhook processed successfully',
      eventId: eventId,
      eventType: eventType,
      eventAction: eventAction
    });
    
  } catch (error) {
    console.error('Error processing webhook:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

/**
 * Root endpoint with basic info
 */
app.get('/', (req, res) => {
  res.json({
    message: 'Nexus Support-a-Creator Webhook Server',
    endpoints: {
      webhook: 'POST /webhook',
    },
    supportedEvents: {
      GroupManagement: ['NewMember', 'RemovedMember']
    }
  });
});

/**
 * Start the server
 */
app.listen(port, () => {
  console.log(`Webhook server running on port ${port}`);
  console.log(`Webhook endpoint: http://localhost:${port}/webhook`);
  
  if (WEBHOOK_SECRET === 'your-webhook-secret-here') {
    console.log('Warning: Using default webhook secret. Set WEBHOOK_SECRET environment variable for production.');
  }
});

module.exports = app;
```
