Webhooks Guide

Real-time event notifications from GreenMonkey to your application.

Overview

Webhooks allow your application to receive real-time notifications when events occur in GreenMonkey. Instead of polling our API, you can subscribe to events and we'll send HTTP POST requests to your endpoint.

Setting Up Webhooks

Step 1: Create Webhook Endpoint

Create an endpoint in your application to receive webhooks:

// Express.js example
app.post('/webhooks/greenmonkey', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-greenmonkey-signature'];
  const timestamp = req.headers['x-greenmonkey-timestamp'];
  const body = req.body;

  // Verify webhook (see Security section)
  if (!verifyWebhook(body, signature, timestamp)) {
    return res.status(400).send('Invalid signature');
  }

  // Parse event
  const event = JSON.parse(body);

  // Handle event
  switch (event.type) {
    case 'purchase.completed':
      handlePurchaseCompleted(event.data);
      break;
    case 'product.published':
      handleProductPublished(event.data);
      break;
    // ... handle other events
  }

  // Acknowledge receipt
  res.status(200).send('OK');
});

Step 2: Configure Webhook in Dashboard

  1. Go to Dashboard → Settings → Webhooks
  2. Click "Add Webhook Endpoint"
  3. Enter your endpoint URL
  4. Select events to subscribe to
  5. Copy the webhook secret

Step 3: Test Your Webhook

Use the test feature to verify your endpoint:

# GreenMonkey will send a test event
POST https://your-app.com/webhooks/greenmonkey
{
  "id": "evt_test_123",
  "type": "test",
  "created": "2024-03-20T10:00:00Z",
  "data": {
    "message": "Test webhook from GreenMonkey"
  }
}

Event Types

Product Events

product.published

Triggered when a new product is published.

{
  "id": "evt_abc123",
  "type": "product.published",
  "created": "2024-03-20T10:00:00Z",
  "data": {
    "product": {
      "id": "prod_123",
      "title": "SEO Content Generator",
      "type": "PROMPT",
      "price": 29.99,
      "authorId": "user_456"
    }
  }
}

product.updated

Triggered when a product is updated.

{
  "id": "evt_def456",
  "type": "product.updated",
  "created": "2024-03-20T11:00:00Z",
  "data": {
    "product": {
      "id": "prod_123",
      "changes": {
        "price": { "old": 29.99, "new": 24.99 },
        "description": { "old": "...", "new": "..." }
      }
    }
  }
}

product.unpublished

Triggered when a product is unpublished or deleted.

Purchase Events

purchase.completed

Triggered when a purchase is successful.

{
  "id": "evt_ghi789",
  "type": "purchase.completed",
  "created": "2024-03-20T12:00:00Z",
  "data": {
    "order": {
      "id": "ord_789",
      "productId": "prod_123",
      "buyerId": "user_101",
      "amount": 24.99,
      "currency": "USD"
    }
  }
}

purchase.refunded

Triggered when a refund is processed.

{
  "id": "evt_jkl012",
  "type": "purchase.refunded",
  "created": "2024-03-20T13:00:00Z",
  "data": {
    "refund": {
      "id": "ref_345",
      "orderId": "ord_789",
      "amount": 24.99,
      "reason": "Customer request"
    }
  }
}

Payout Events

payout.completed

Triggered when a seller payout is sent.

{
  "id": "evt_mno345",
  "type": "payout.completed",
  "created": "2024-03-20T14:00:00Z",
  "data": {
    "payout": {
      "id": "pay_678",
      "sellerId": "user_456",
      "amount": 199.92,
      "currency": "USD",
      "period": {
        "start": "2024-03-01",
        "end": "2024-03-19"
      }
    }
  }
}

Review Events

review.created

Triggered when a new review is posted.

{
  "id": "evt_pqr678",
  "type": "review.created",
  "created": "2024-03-20T15:00:00Z",
  "data": {
    "review": {
      "id": "rev_901",
      "productId": "prod_123",
      "rating": 5,
      "comment": "Excellent prompt!",
      "authorId": "user_101"
    }
  }
}

User Events

user.subscription.updated

Triggered when a user's subscription changes.

{
  "id": "evt_stu901",
  "type": "user.subscription.updated",
  "created": "2024-03-20T16:00:00Z",
  "data": {
    "subscription": {
      "userId": "user_101",
      "oldPlan": "free",
      "newPlan": "pro",
      "effectiveDate": "2024-03-20T16:00:00Z"
    }
  }
}

Webhook Security

Signature Verification

Always verify webhook signatures to ensure requests are from GreenMonkey:

const crypto = require('crypto');

function verifyWebhook(payload, signature, timestamp, secret) {
  // Check timestamp (prevent replay attacks)
  const currentTime = Math.floor(Date.now() / 1000);
  if (currentTime - parseInt(timestamp) > 300) {
    // 5 minutes
    return false;
  }

  // Create signature
  const signedPayload = `${timestamp}.${payload}`;
  const expectedSignature = crypto.createHmac('sha256', secret).update(signedPayload).digest('hex');

  // Compare signatures
  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature));
}

IP Allowlisting

For additional security, allowlist GreenMonkey's webhook IPs:

35.241.123.45
35.241.123.46
35.241.123.47

HTTPS Only

Always use HTTPS endpoints for webhooks:

  • āœ… https://your-app.com/webhooks
  • āŒ http://your-app.com/webhooks

Handling Webhooks

Idempotency

Webhooks may be sent multiple times. Handle idempotently:

const processedEvents = new Set();

async function handleWebhook(event) {
  // Check if already processed
  if (processedEvents.has(event.id)) {
    console.log(`Event ${event.id} already processed`);
    return;
  }

  // Process event
  await processEvent(event);

  // Mark as processed
  processedEvents.add(event.id);

  // Store in database for persistence
  await db.processedWebhooks.create({
    data: { eventId: event.id },
  });
}

Async Processing

Process webhooks asynchronously to respond quickly:

app.post('/webhooks', async (req, res) => {
  const event = req.body;

  // Acknowledge immediately
  res.status(200).send('OK');

  // Process async
  queue.add('process-webhook', { event });
});

// Worker
queue.process('process-webhook', async (job) => {
  const { event } = job.data;

  switch (event.type) {
    case 'purchase.completed':
      await sendThankYouEmail(event.data);
      await updateInventory(event.data);
      break;
    // ... other handlers
  }
});

Error Handling

Implement robust error handling:

async function handleWebhook(event) {
  try {
    await processEvent(event);
  } catch (error) {
    console.error('Webhook processing error:', error);

    // Log to monitoring service
    await logError({
      type: 'webhook_error',
      event: event.id,
      error: error.message,
    });

    // Retry if appropriate
    if (isRetryableError(error)) {
      await retryQueue.add({ event, attempts: 1 });
    }
  }
}

Retry Logic

GreenMonkey implements automatic retries for failed webhooks:

Retry Schedule

  1. Immediate retry
  2. 1 minute
  3. 5 minutes
  4. 30 minutes
  5. 2 hours
  6. 6 hours
  7. 12 hours
  8. 24 hours

After 8 attempts, the webhook is marked as failed.

Handling Retries

app.post('/webhooks', (req, res) => {
  const attemptNumber = req.headers['x-greenmonkey-attempt'];

  if (attemptNumber > 1) {
    console.log(`Retry attempt ${attemptNumber} for event ${req.body.id}`);
  }

  // Process webhook...
});

Development & Testing

Local Development

Use ngrok for local webhook testing:

# Install ngrok
npm install -g ngrok

# Expose local server
ngrok http 3000

# Use the HTTPS URL for webhooks
# https://abc123.ngrok.io/webhooks

Webhook CLI

Test webhooks with our CLI:

# Install CLI
npm install -g @greenmonkey/cli

# Send test event
greenmonkey webhooks test \
  --event purchase.completed \
  --endpoint https://your-app.com/webhooks

Mock Events

Generate mock events for testing:

const mockEvents = {
  'purchase.completed': {
    id: 'evt_test_123',
    type: 'purchase.completed',
    created: new Date().toISOString(),
    data: {
      order: {
        id: 'ord_test_123',
        productId: 'prod_test_123',
        buyerId: 'user_test_123',
        amount: 29.99,
        currency: 'USD',
      },
    },
  },
};

// Test your handler
handleWebhook(mockEvents['purchase.completed']);

Monitoring & Debugging

Webhook Logs

View webhook delivery logs in the dashboard:

  1. Go to Dashboard → Settings → Webhooks
  2. Click on your endpoint
  3. View delivery history
  4. See request/response details

Debug Information

Each webhook includes debug headers:

X-GreenMonkey-Event-ID: evt_abc123
X-GreenMonkey-Timestamp: 1679308800
X-GreenMonkey-Signature: sha256=abc...
X-GreenMonkey-Attempt: 1

Common Issues

Webhook not received

  • Verify endpoint URL is correct
  • Check server logs for errors
  • Ensure HTTPS is working
  • Check firewall rules

Signature verification failing

  • Ensure using raw body (not parsed)
  • Verify secret is correct
  • Check timestamp validation

Duplicate events

  • Implement idempotency
  • Check for event ID in database
  • Handle gracefully

Best Practices

Do's āœ…

  1. Verify signatures - Always validate webhook authenticity
  2. Respond quickly - Return 200 OK immediately
  3. Process async - Handle processing in background
  4. Be idempotent - Handle duplicate events gracefully
  5. Log everything - Keep detailed logs for debugging
  6. Monitor failures - Alert on webhook processing errors

Don'ts āŒ

  1. Don't trust blindly - Always verify signatures
  2. Don't process synchronously - Will timeout
  3. Don't ignore retries - Handle them properly
  4. Don't expose secrets - Keep webhook secret secure
  5. Don't ignore errors - Log and monitor failures

Example Implementations

Node.js + Express

const express = require('express');
const crypto = require('crypto');

const app = express();
const WEBHOOK_SECRET = process.env.GREENMONKEY_WEBHOOK_SECRET;

app.post('/webhooks/greenmonkey', express.raw({ type: 'application/json' }), async (req, res) => {
  // Verify webhook
  const signature = req.headers['x-greenmonkey-signature'];
  if (!verifySignature(req.body, signature, WEBHOOK_SECRET)) {
    return res.status(400).send('Invalid signature');
  }

  // Parse and handle
  const event = JSON.parse(req.body);
  await handleEvent(event);

  res.status(200).send('OK');
});

Python + Flask

from flask import Flask, request, abort
import hmac
import hashlib
import json

app = Flask(__name__)
WEBHOOK_SECRET = os.environ['GREENMONKEY_WEBHOOK_SECRET']

@app.route('/webhooks/greenmonkey', methods=['POST'])
def handle_webhook():
    # Verify signature
    signature = request.headers.get('X-GreenMonkey-Signature')
    if not verify_signature(request.data, signature, WEBHOOK_SECRET):
        abort(400)

    # Parse event
    event = json.loads(request.data)

    # Handle event
    handle_event(event)

    return 'OK', 200

def verify_signature(payload, signature, secret):
    expected = hmac.new(
        secret.encode(),
        payload,
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(expected, signature)

Ruby + Rails

class WebhooksController < ApplicationController
  skip_before_action :verify_authenticity_token

  def greenmonkey
    # Verify signature
    signature = request.headers['X-GreenMonkey-Signature']
    unless verify_signature(request.body.read, signature)
      return head :bad_request
    end

    # Parse and handle
    event = JSON.parse(request.body.read)
    WebhookJob.perform_later(event)

    head :ok
  end

  private

  def verify_signature(payload, signature)
    expected = OpenSSL::HMAC.hexdigest(
      'SHA256',
      ENV['GREENMONKEY_WEBHOOK_SECRET'],
      payload
    )

    Rack::Utils.secure_compare(expected, signature)
  end
end

Next Steps