---
title: Webhooks
priority: 6
---
import { MDXCodeBlocks } from '~/src/components/MDXCodeBlocks'
import { RyuNotice } from '@ramp/ryu'
## Overview
Webhooks allow your application to receive real-time notifications about events that occur in your Ramp account. Instead of polling our API, webhooks push notifications to your specified endpoint whenever an event happens.
**Key benefits:**
- Instant notification of events like transactions, approvals, or reimbursements
- Reduces the need for polling APIs to check for updates
- Enables automation of downstream workflows (e.g., syncing with accounting software)
---
## Available Events
Webhooks are organized by resource type. Your access token must include the appropriate scope to subscribe to events for each resource.
| **Resource** | **Required Scope** | **Event** | **Description** |
|--------------|-------------------|-----------|-----------------|
| **Transactions** | `transactions:read` | `transactions.ready_for_review` | Transaction needs review or coding |
| | | `transactions.cleared` | Transaction has been settled |
| | | `transactions.authorized` | Transaction has been authorized |
| | | `transactions.ready_to_sync` | Transaction is ready to sync to accounting system |
| **Reimbursements** | `reimbursements:read` | `reimbursements.ready_for_review` | Reimbursement needs review or coding |
| | | `reimbursements.ready_to_sync` | Reimbursement is ready to sync to accounting system |
| **Bills** | `bills:read` | `bills.created` | New bill has been created |
| | | `bills.approved` | Bill has been approved for payment |
| | | `bills.rejected` | Bill has been rejected |
| | | `bills.paid` | Bill payment has been completed |
| **Purchase Orders** | `purchase_orders:read` | `purchase_orders.created` | New purchase order has been created |
| | | `purchase_orders.updated` | Purchase order has been modified |
---
## Getting Started
### 1. Create a Webhook Endpoint
Set up an HTTPS endpoint (URL) on your server that can receive `POST` requests from Ramp. Your endpoint must:
- Be publicly accessible
- Use HTTPS
- Return a 2xx status code for successful receipt
- Process requests within 10 seconds
<MDXCodeBlocks title="Basic webhook endpoint">
```bash
# Test your endpoint is reachable
curl -X POST https://your-domain.com/webhooks \
-H "Content-Type: application/json" \
-d '{"test": "webhook"}'
```
```js
// Express.js example
const express = require('express')
const app = express()
app.use(express.json())
app.post('/webhooks', (req, res) => {
console.log('Received webhook:', req.body)
// Process the webhook payload
const { id, type, object } = req.body
// Always respond quickly with 2xx status
res.status(200).json({ received: true })
// Handle the event asynchronously
processWebhookAsync(req.body)
})
async function processWebhookAsync(payload) {
// Your business logic here
}
app.listen(3000)
```
```python
# Flask example
from flask import Flask, request, jsonify
import threading
app = Flask(__name__)
@app.route('/webhooks', methods=['POST'])
def handle_webhook():
data = request.get_json()
print(f"Received webhook: {data}")
# Always respond quickly with 2xx status
response = jsonify({"received": True})
# Process webhook asynchronously
thread = threading.Thread(target=process_webhook_async, args=(data,))
thread.start()
return response, 200
def process_webhook_async(payload):
# Your business logic here
pass
if __name__ == '__main__':
app.run(host='0.0.0.0', port=3000)
```
</MDXCodeBlocks>
### 2. Subscribe to Events
Create a webhook subscription using the `/webhooks` endpoint. You must have the appropriate scopes in your access token.
<MDXCodeBlocks title="Subscribe to webhook events">
```bash
curl -X POST https://api.ramp.com/developer/v1/webhooks \
-H "Authorization: Bearer $RAMP_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"endpoint_url": "https://your-domain.com/webhooks",
"event_types": ["transactions.authorized", "transactions.cleared"]
}'
```
```js
const response = await fetch('https://api.ramp.com/developer/v1/webhooks', {
method: 'POST',
headers: {
'Authorization': `Bearer ${RAMP_API_TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
endpoint_url: 'https://your-domain.com/webhooks',
event_types: ['transactions.authorized', 'transactions.cleared']
})
})
const webhook = await response.json()
console.log('Created webhook:', webhook)
```
```python
import requests
headers = {
"Authorization": f"Bearer {RAMP_API_TOKEN}",
"Content-Type": "application/json"
}
payload = {
"endpoint_url": "https://your-domain.com/webhooks",
"event_types": ["transactions.authorized", "transactions.cleared"]
}
response = requests.post(
"https://api.ramp.com/developer/v1/webhooks",
headers=headers,
json=payload
)
webhook = response.json()
print(f"Created webhook: {webhook}")
```
</MDXCodeBlocks>
### 3. Verify Your Webhook
Ramp will send a `POST` request with a challenge to your endpoint during setup.
1. Receive the `challenge` string from Ramp
2. Respond by making a verification API call:
<MDXCodeBlocks title="Verify webhook endpoint">
```bash
curl -X POST https://api.ramp.com/developer/v1/webhooks/{webhook_id}/verify \
-H "Authorization: Bearer $RAMP_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"challenge": "CHALLENGE_STRING_FROM_RAMP"
}'
```
```js
const response = await fetch(`https://api.ramp.com/developer/v1/webhooks/${webhookId}/verify`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${RAMP_API_TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
challenge: 'CHALLENGE_STRING_FROM_RAMP'
})
})
const result = await response.json()
console.log('Verification result:', result)
```
```python
verification_response = requests.post(
f"https://api.ramp.com/developer/v1/webhooks/{webhook_id}/verify",
headers=headers,
json={"challenge": "CHALLENGE_STRING_FROM_RAMP"}
)
print(f"Verification result: {verification_response.json()}")
```
</MDXCodeBlocks>
---
## Webhook Payload Format
All webhook payloads follow this consistent structure:
```json
{
"id": "a461a658-8d78-4e66-b1fd-9c818bcfea6b",
"type": "transactions.cleared",
"created_at": "2025-04-01T12:00:00Z",
"business_id": "945c7d8f-6c35-4d63-9c1c-79e67c9cee73",
"object": {
"id": "<resource-id>"
}
}
```
**Payload fields:**
- `id`: Unique event ID (constant across retries)
- `type`: Event type from the available events table
- `created_at`: Event timestamp in ISO 8601 format
- `business_id`: Identifies which business the event belongs to
- `object`: Contains the affected resource ID
### Example Payloads
**Transaction Event:**
```json
{
"id": "a461a658-8d78-4e66-b1fd-9c818bcfea6b",
"type": "transactions.cleared",
"created_at": "2025-04-01T12:00:00Z",
"business_id": "945c7d8f-6c35-4d63-9c1c-79e67c9cee73",
"object": {
"id": "3aa9eed1-c793-4b52-a502-e7f2a7e4c1b2"
}
}
```
**Reimbursement Event:**
```json
{
"id": "b572f759-9e89-4f77-c614-0d929fdcfb7d",
"type": "reimbursements.ready_to_sync",
"created_at": "2025-04-01T12:05:00Z",
"business_id": "945c7d8f-6c35-4d63-9c1c-79e67c9cee73",
"object": {
"id": "8bc2e447-f894-4d23-9c15-2e8f7b6a3c1d"
}
}
```
**Bill Event:**
```json
{
"id": "c683g860-0f90-5g88-d725-1f040gegdg8e",
"type": "bills.ready_to_sync",
"created_at": "2025-04-01T12:10:00Z",
"business_id": "945c7d8f-6c35-4d63-9c1c-79e67c9cee73",
"object": {
"id": "9cd3f558-g005-5e34-0d26-3f9g8c7b4d2e"
}
}
```
**Purchase Order Event:**
```json
{
"id": "d794h971-1g01-6h99-e836-2g151hgghg9f",
"type": "purchase_orders.ready_to_sync",
"created_at": "2025-04-01T12:15:00Z",
"business_id": "945c7d8f-6c35-4d63-9c1c-79e67c9cee73",
"object": {
"id": "0de4g669-h116-6f45-1e37-4g0h9d8c5e3f"
}
}
```
To get the full resource details, use the resource ID to fetch from the appropriate API endpoint (e.g., `/transactions/{id}` for transaction events, `/reimbursements/{id}` for reimbursement events).
---
## Verifying Webhook Signatures
Each webhook request contains a signature in the `X-Ramp-Webhook-Signature` header for security verification.
<MDXCodeBlocks title="Verify webhook signatures">
```bash
# Signature verification should be done in your application code
# This is just an example of extracting the header
curl -v https://your-domain.com/webhooks \
-H "X-Ramp-Webhook-Signature: expected_signature_here"
```
```js
const crypto = require('crypto')
function verifyWebhookSignature(payload, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('hex')
return crypto.timingSafeEqual(
Buffer.from(expectedSignature, 'hex'),
Buffer.from(signature, 'hex')
)
}
// In your webhook handler
app.post('/webhooks', (req, res) => {
const signature = req.headers['x-ramp-webhook-signature']
const payload = JSON.stringify(req.body)
if (!verifyWebhookSignature(payload, signature, WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature')
}
// Process webhook...
res.status(200).json({ received: true })
})
```
```python
import hmac
import hashlib
def verify_webhook_signature(payload, signature, secret):
expected_signature = hmac.new(
secret.encode('utf-8'),
payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected_signature, signature)
# In your Flask route
@app.route('/webhooks', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-Ramp-Webhook-Signature')
payload = request.get_data(as_text=True)
if not verify_webhook_signature(payload, signature, WEBHOOK_SECRET):
return jsonify({"error": "Invalid signature"}), 401
data = request.get_json()
# Process webhook...
return jsonify({"received": True}), 200
```
</MDXCodeBlocks>
---
## Delivery and Retry Logic
### Rate Limiting
Webhook delivery throughput is not limited on Ramp's end. However, if your endpoint returns a `429 Too Many Requests` status, Ramp has built-in mitigations to handle rate limiting gracefully.
### Retry Schedule
Ramp's retry behavior depends on the HTTP status code returned by your webhook endpoint:
| Status Code | Behavior |
|-------------|----------|
| **2xx** | Delivery successful - no retries |
| **3xx, 4xx** (except 429) | Instant delivery failure - no retries |
| **429, 5xx** (including timeouts) | Retry up to **10 times** with exponential backoff |
**Retry Timing:** Failed deliveries use exponential backoff with jitter, starting between 0-60 seconds and growing exponentially with each failed attempt. Webhook requests timeout after 10 seconds.
**Important:** The same event `id` is used for all retry attempts, allowing you to implement idempotency checks.
**Event Ordering:** Webhooks may arrive out of order. For example:
- `transactions.authorized` might be delayed or retried
- `transactions.cleared` may arrive earlier if successful
Design your system to handle **out-of-order events** gracefully by checking the event timestamp and fetching the latest resource state when needed.
---
## Best Practices
### Reliability
- **Implement idempotency checks** using the event `id` to handle duplicate deliveries
- **Return 2xx responses within 10 seconds** to avoid timeout retries
- **Process payloads asynchronously** to respond quickly
- **Log raw payloads** for debugging and audit trails
- **Handle retries gracefully** - the same event may be delivered up to 10 times for 429/5xx responses
### Security
- **Always verify webhook signatures** before processing
- **Use HTTPS endpoints** to encrypt data in transit
- **Validate event types** against your expected events
### Error Handling
- Implement proper error handling for malformed payloads
- Monitor for failed webhook deliveries
- Set up alerting for webhook endpoint downtime
<MDXCodeBlocks title="Robust webhook handler example">
```bash
# Monitor your webhook endpoint health
curl -f https://your-domain.com/health || echo "Webhook endpoint is down!"
```
```js
const processedEvents = new Set()
app.post('/webhooks', async (req, res) => {
try {
const { id, type, object, business_id } = req.body
// Check for duplicate events
if (processedEvents.has(id)) {
console.log(`Duplicate event ${id}, skipping`)
return res.status(200).json({ received: true })
}
// Verify signature
const signature = req.headers['x-ramp-webhook-signature']
if (!verifyWebhookSignature(JSON.stringify(req.body), signature, WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature')
}
// Respond immediately
res.status(200).json({ received: true })
// Process asynchronously
processWebhookAsync({ id, type, object, business_id })
processedEvents.add(id)
} catch (error) {
console.error('Webhook processing error:', error)
res.status(500).send('Internal server error')
}
})
```
```python
processed_events = set()
@app.route('/webhooks', methods=['POST'])
def handle_webhook():
try:
data = request.get_json()
event_id = data.get('id')
# Check for duplicate events
if event_id in processed_events:
print(f"Duplicate event {event_id}, skipping")
return jsonify({"received": True}), 200
# Verify signature
signature = request.headers.get('X-Ramp-Webhook-Signature')
payload = request.get_data(as_text=True)
if not verify_webhook_signature(payload, signature, WEBHOOK_SECRET):
return jsonify({"error": "Invalid signature"}), 401
# Respond immediately
response = jsonify({"received": True})
# Process asynchronously
thread = threading.Thread(target=process_webhook_async, args=(data,))
thread.start()
processed_events.add(event_id)
return response, 200
except Exception as e:
print(f"Webhook processing error: {e}")
return jsonify({"error": "Internal server error"}), 500
```
</MDXCodeBlocks>
---
## Managing Webhook Subscriptions
### List All Subscriptions
<MDXCodeBlocks title="List webhook subscriptions">
```bash
curl -X GET https://api.ramp.com/developer/v1/webhooks \
-H "Authorization: Bearer $RAMP_API_TOKEN"
```
```js
const response = await fetch('https://api.ramp.com/developer/v1/webhooks', {
headers: {
'Authorization': `Bearer ${RAMP_API_TOKEN}`
}
})
const webhooks = await response.json()
console.log('Webhook subscriptions:', webhooks)
```
```python
response = requests.get(
"https://api.ramp.com/developer/v1/webhooks",
headers={"Authorization": f"Bearer {RAMP_API_TOKEN}"}
)
webhooks = response.json()
print(f"Webhook subscriptions: {webhooks}")
```
</MDXCodeBlocks>
### Retrieve Webhook Details
<MDXCodeBlocks title="Get webhook details">
```bash
curl -X GET https://api.ramp.com/developer/v1/webhooks/{webhook_id} \
-H "Authorization: Bearer $RAMP_API_TOKEN"
```
```js
const response = await fetch(`https://api.ramp.com/developer/v1/webhooks/${webhookId}`, {
headers: {
'Authorization': `Bearer ${RAMP_API_TOKEN}`
}
})
const webhook = await response.json()
console.log('Webhook details:', webhook)
```
```python
response = requests.get(
f"https://api.ramp.com/developer/v1/webhooks/{webhook_id}",
headers={"Authorization": f"Bearer {RAMP_API_TOKEN}"}
)
webhook = response.json()
print(f"Webhook details: {webhook}")
```
</MDXCodeBlocks>
### Delete a Webhook
<MDXCodeBlocks title="Delete webhook subscription">
```bash
curl -X DELETE https://api.ramp.com/developer/v1/webhooks/{webhook_id} \
-H "Authorization: Bearer $RAMP_API_TOKEN"
```
```js
const response = await fetch(`https://api.ramp.com/developer/v1/webhooks/${webhookId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${RAMP_API_TOKEN}`
}
})
console.log('Webhook deleted:', response.status === 204)
```
```python
response = requests.delete(
f"https://api.ramp.com/developer/v1/webhooks/{webhook_id}",
headers={"Authorization": f"Bearer {RAMP_API_TOKEN}"}
)
print(f"Webhook deleted: {response.status_code == 204}")
```
</MDXCodeBlocks>
---
## For Third-Party Integrators
Use the same `/webhooks` endpoints to manage subscriptions for multi-customer applications.
The webhook payload always includes a `business_id`, which can be used to identify the associated business:
<MDXCodeBlocks title="Handle multi-tenant webhooks">
```bash
# Use business_id to route events to the correct customer
curl -X GET https://api.ramp.com/developer/v1/business \
-H "Authorization: Bearer $CUSTOMER_SPECIFIC_TOKEN"
```
```js
// Route webhook to correct customer based on business_id
app.post('/webhooks', (req, res) => {
const { business_id, type, object } = req.body
// Route to customer-specific handler
const customerHandler = getCustomerHandler(business_id)
res.status(200).json({ received: true })
customerHandler.processWebhook({ type, object })
})
function getCustomerHandler(businessId) {
// Your customer routing logic
return customerHandlers[businessId]
}
```
```python
@app.route('/webhooks', methods=['POST'])
def handle_webhook():
data = request.get_json()
business_id = data.get('business_id')
# Route to customer-specific handler
customer_handler = get_customer_handler(business_id)
response = jsonify({"received": True})
customer_handler.process_webhook(data)
return response, 200
def get_customer_handler(business_id):
# Your customer routing logic
return customer_handlers.get(business_id)
```
</MDXCodeBlocks>
---
## Troubleshooting
:::warning[Questions? Contact us at developer-support@ramp.com]
:::
#### Why am I not receiving webhooks?
- Verify your endpoint is publicly accessible and returns 2xx status codes
- Check that your webhook subscription includes the correct event types
- Ensure your access token has the required scopes for the events
- Confirm your endpoint responds within 10 seconds
---
#### What happens if my server was down?
- Ramp will retry webhook deliveries with exponential backoff
- Missed events during extended downtime may not be recoverable
- Consider implementing a fallback polling mechanism for critical events
---
#### How do I change my webhook URL?
- Delete the existing webhook subscription
- Create a new subscription with the updated URL
- Re-verify the new endpoint
---
#### How do I test webhooks in development?
- Use tools like [ngrok](https://ngrok.com/) to expose your local server
- Use [webhook.site](https://webhook.site/) to get a temporary URL for testing webhook delivery
- Test with a small subset of event types initially
- Monitor webhook logs for successful delivery and processing
---
#### Will there be a dashboard to view delivery logs?
- A developer dashboard for webhook monitoring is coming soon
- For now, implement comprehensive logging in your webhook handlers
---
## Next Steps
Ready to implement webhooks in your integration? Here are some related guides:
- **Accounting Integration**: Learn how `ready_to_sync` webhooks can replace polling in your [Accounting Integration](/developer-api/v1/guides/accounting)
- **Getting Started**: Set up authentication and make your first API calls with our [Getting Started Guide](/developer-api/v1/guides/getting-started)
- **Bill Pay**: Automate bill payment workflows with our [Bill Pay Guide](/developer-api/v1/guides/bill-pay)
If you have questions or feedback about webhooks, email [developer-support@ramp.com](mailto:developer-support@ramp.com) – we're here to help make your integration successful.