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. Go to Dashboard → Developers → Webhooks
- 2. Click "Add Endpoint"
- 3. Enter your endpoint URL (e.g., https://yoursite.com/webhooks/hedge)
- 4. Select the events you want to receive
- 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', 200Event Types
Payment Events
| Event | Description |
|---|---|
| payment.created | A new payment intent was created |
| payment.processing | Payment is being processed |
| payment.succeeded | Payment was successful |
| payment.failed | Payment failed |
| payment.refunded | Payment was refunded (full or partial) |
ACH Events
| Event | Description |
|---|---|
| ach.initiated | ACH debit was initiated |
| ach.processing | ACH is being processed by the bank |
| ach.succeeded | ACH debit completed successfully |
| ach.failed | ACH debit failed |
| ach.returned | ACH was returned after initial success |
Subscription Events
| Event | Description |
|---|---|
| subscription.created | New subscription was created |
| subscription.updated | Subscription was modified |
| subscription.canceled | Subscription was canceled |
| subscription.payment_failed | Recurring payment failed |
Checkout Events
| Event | Description |
|---|---|
| checkout.completed | Checkout session was completed |
| checkout.expired | Checkout 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:
| Attempt | Delay |
|---|---|
| 1st retry | 1 minute |
| 2nd retry | 5 minutes |
| 3rd retry | 30 minutes |
| 4th retry | 2 hours |
| 5th retry (final) | 24 hours |
After 5 failed attempts, the webhook is marked as failed and you'll receive an email notification.