Guides/Webhooks

Webhooks

Receive real-time notifications for payment events

What are Webhooks?

Webhooks are HTTP callbacks that notify your server when events occur in your Hedge Payments account - like successful payments, failed transactions, or subscription changes. They allow you to automate workflows without polling our API.

Setting Up Webhooks

1. Create an Endpoint

Create an HTTP endpoint on your server to receive webhook events:

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

  // Process the webhook
  console.log('Received webhook:', payload)

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

2. Register in Dashboard

  1. 1. Go to Dashboard → Developers → Webhooks
  2. 2. Click "Add Endpoint"
  3. 3. Enter your endpoint URL (e.g., https://yoursite.com/webhooks/hedge)
  4. 4. Select the events you want to receive
  5. 5. Copy your webhook signing secret

Verifying Signatures

Always verify webhook signatures to ensure requests are from Hedge Payments:

⚠️ Security: Never process webhooks without verifying the signature. Attackers could send fake events to your endpoint.

Node.js

import { HedgePayments } from '@hedgepayments/node'

const hedge = new HedgePayments({
  apiKey: process.env.HEDGE_API_KEY,
  apiSecret: process.env.HEDGE_API_SECRET
})

const webhookSecret = process.env.HEDGE_WEBHOOK_SECRET

app.post('/webhooks/hedge', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-hedge-signature']

  let event
  try {
    event = hedge.webhooks.verify(req.body, signature, webhookSecret)
  } catch (err) {
    console.error('Webhook signature verification failed:', err.message)
    return res.status(400).send('Invalid signature')
  }

  // Process verified event
  console.log('Verified event:', event.type)

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

Python

from hedgepayments import HedgePayments
import os

hedge = HedgePayments(
    api_key=os.environ['HEDGE_API_KEY'],
    api_secret=os.environ['HEDGE_API_SECRET']
)

webhook_secret = os.environ['HEDGE_WEBHOOK_SECRET']

@app.route('/webhooks/hedge', methods=['POST'])
def hedge_webhook():
    payload = request.data
    signature = request.headers.get('X-Hedge-Signature')

    try:
        event = hedge.webhooks.verify(payload, signature, webhook_secret)
    except Exception as e:
        return 'Invalid signature', 400

    # Process verified event
    print(f'Verified event: {event.type}')

    return 'OK', 200

Event Types

Payment Events

EventDescription
payment.createdA new payment intent was created
payment.processingPayment is being processed
payment.succeededPayment was successful
payment.failedPayment failed
payment.refundedPayment was refunded (full or partial)

ACH Events

EventDescription
ach.initiatedACH debit was initiated
ach.processingACH is being processed by the bank
ach.succeededACH debit completed successfully
ach.failedACH debit failed
ach.returnedACH was returned after initial success

Subscription Events

EventDescription
subscription.createdNew subscription was created
subscription.updatedSubscription was modified
subscription.canceledSubscription was canceled
subscription.payment_failedRecurring payment failed

Checkout Events

EventDescription
checkout.completedCheckout session was completed
checkout.expiredCheckout link expired without payment

Event Payload

All webhook events follow this structure:

{
  "id": "evt_abc123xyz",
  "type": "payment.succeeded",
  "created": "2024-01-15T10:30:00Z",
  "livemode": false,
  "data": {
    "id": "pay_xyz789",
    "amount": 9999,
    "currency": "USD",
    "status": "succeeded",
    "customer_email": "customer@example.com",
    "payment_method": "card",
    "metadata": {
      "orderId": "order_123"
    },
    "created_at": "2024-01-15T10:29:45Z"
  }
}

Handling Events

Example of handling different event types:

app.post('/webhooks/hedge', async (req, res) => {
  const event = hedge.webhooks.verify(req.body, req.headers['x-hedge-signature'])

  switch (event.type) {
    case 'payment.succeeded':
      await handlePaymentSuccess(event.data)
      break

    case 'payment.failed':
      await handlePaymentFailure(event.data)
      break

    case 'ach.returned':
      await handleACHReturn(event.data)
      break

    case 'subscription.canceled':
      await handleSubscriptionCanceled(event.data)
      break

    default:
      console.log(`Unhandled event type: ${event.type}`)
  }

  // Always return 200 to acknowledge receipt
  res.status(200).send('OK')
})

async function handlePaymentSuccess(payment) {
  // Update order status
  await db.orders.update({
    where: { id: payment.metadata.orderId },
    data: { status: 'paid', paidAt: new Date() }
  })

  // Send confirmation email
  await sendEmail({
    to: payment.customer_email,
    template: 'payment_confirmation',
    data: { amount: payment.amount / 100 }
  })
}

async function handlePaymentFailure(payment) {
  // Notify customer of failed payment
  await sendEmail({
    to: payment.customer_email,
    template: 'payment_failed',
    data: { reason: payment.failure_reason }
  })
}

Best Practices

  • Always verify signatures - Never process unverified webhooks
  • Return 200 quickly - Process events asynchronously if needed
  • Handle duplicates - Use event IDs to deduplicate (webhooks may be retried)
  • Log all events - Keep records for debugging and auditing
  • Use idempotent handlers - Same event processed twice should have same result
  • Monitor failures - Set up alerts for webhook delivery issues

Retry Policy

If your endpoint doesn't return a 2xx response, we'll retry with exponential backoff:

AttemptDelay
1st retry1 minute
2nd retry5 minutes
3rd retry30 minutes
4th retry2 hours
5th retry (final)24 hours

After 5 failed attempts, the webhook is marked as failed and you'll receive an email notification.