Skip to main content
Glama
riker-t

Ramp Developer MCP Server

by riker-t
webhooks.mdx19.9 kB
--- 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.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/riker-t/ramp-dev-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server