Agree.com Webhooks API
Receive real-time notifications when events occur in your Agree account. ## Overview Webhooks push event data to your application as soon as something happens - like when an invoice is paid or a payment fails. This eliminates the need to poll the API for updates and lets you respond to events instantly. **Key concepts:** - You register a URL endpoint that Agree will call when events occur - Each endpoint can subscribe to specific event types - Webhook payloads are signed so you can verify they came from Agree - Failed deliveries are automatically retried with exponential backoff ## Quick Setup ### 1. Create an Endpoint Register a URL to receive webhooks: ```bash curl -X POST https://api.agree.com/api/v1/webhook_endpoints \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "webhook_endpoint": { "url": "https://your-app.com/webhooks/agree", "events": ["invoice.paid", "invoice.failed"] } }' ``` **Response:** ```json { "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "url": "https://your-app.com/webhooks/agree", "events": ["invoice.paid", "invoice.failed"], "active": true, "failure_count": 0, "secret": "whsec_abc123xyz789...", "inserted_at": "2025-01-15T10:30:00Z", "updated_at": "2025-01-15T10:30:00Z" } } ``` **Important:** Save the `secret` - it's only returned once at creation. You'll need it to verify webhook signatures. ### 2. Handle Incoming Webhooks When an event occurs, Agree sends a POST request to your endpoint: ```json { "event": "invoice.paid", "payload": { "id": "4a755746-ba45-4226-a669-aebc7ad3719c", "status": "paid", "amount": {"amount": 15000, "currency": "USD"}, "paid_at": "2025-01-20T14:30:00Z" } } ``` ### 3. Verify the Signature Always verify webhooks came from Agree before processing them. See [Verifying Webhooks](#verifying-webhooks) below. ### 4. Test Your Integration Send a test webhook to verify your endpoint works: ```bash curl -X POST https://api.agree.com/api/v1/webhook_endpoints/550e8400-e29b-41d4-a716-446655440000/test \ -H "Authorization: Bearer YOUR_API_KEY" ``` ## Available Events Subscribe to the events your application needs: | Event | Description | When it fires | |-------|-------------|---------------| | `invoice.created` | Invoice was created | After POST /invoices | | `invoice.sent` | Invoice emailed to customer | When delivery completes | | `invoice.due` | Invoice reached due status | When status becomes `due`: either the scheduled job runs after `sent` (past `due_at`), or the invoice is sent already at/past `due_at` (immediate `due`) | | `invoice.paid` | Payment successful | After payment confirmation | | `invoice.failed` | Payment attempt failed | After payment rejection | | `invoice.canceled` | Invoice was canceled | After DELETE /invoices | | `invoice.refunded` | Invoice payment was refunded | After refund is processed | | `agreement.created` | Agreement was created | After POST /agreements | | `agreement.sent` | Agreement was sent to recipients | When status changes to 'sent' | | `agreement.signed` | Agreement was signed by a recipient | When a recipient signs | | `agreement.executed` | Agreement was fully executed | When all signers have signed | | `webhook.test` | Test event | When you trigger a test | **Tip:** Start with `invoice.paid` and `invoice.failed` - these are the most important for payment integrations. ## Webhook Payload Each webhook request includes: ### Headers | Header | Description | |--------|-------------| | `Content-Type` | `application/json` | | `X-Webhook-Signature` | HMAC-SHA256 signature (hex, lowercase) | | `X-Webhook-Timestamp` | Unix timestamp when sent | ### Body ```json { "event": "invoice.paid", "payload": { // Full invoice object with all fields } } ``` The `payload` contains the complete resource object, so you have all the data you need without making additional API calls. For agreement events (`agreement.created`, `agreement.sent`, `agreement.signed`, `agreement.executed`), the payload includes the `GET /api/v1/agreements/:id` response `data` fields — including flat recipient fields such as `recipients[].email` and `recipients[].name`, and `field_values` (a map of `field_id` to filled plain-text values). Webhooks also include nested `recipients[].user` and `recipients[].contact` objects for backwards compatibility; prefer the flat fields for new integrations. ## Verifying Webhooks **Always verify webhook signatures** before processing. This ensures the request actually came from Agree and wasn't tampered with. ### How Verification Works 1. Get the raw request body (before JSON parsing) 2. Compute HMAC-SHA256 using your endpoint's `secret` as the key 3. Hex-encode the result (lowercase) 4. Compare to the `X-Webhook-Signature` header using constant-time comparison ### Node.js Example ```javascript const crypto = require('crypto'); function verifyWebhookSignature(rawBody, signatureHeader, secret) { const expectedSignature = crypto .createHmac('sha256', secret) .update(rawBody) .digest('hex'); return crypto.timingSafeEqual( Buffer.from(expectedSignature), Buffer.from(signatureHeader) ); } // Express middleware example app.post('/webhooks/agree', express.raw({type: 'application/json'}), (req, res) => { const signature = req.headers['x-webhook-signature']; if (!verifyWebhookSignature(req.body, signature, process.env.AGREE_WEBHOOK_SECRET)) { return res.status(401).send('Invalid signature'); } const event = JSON.parse(req.body); switch (event.event) { case 'invoice.paid': // Handle successful payment break; case 'invoice.failed': // Handle failed payment break; } res.status(200).send('OK'); }); ``` ### Python Example ```python import hmac import hashlib from flask import Flask, request app = Flask(__name__) def verify_webhook_signature(raw_body, signature_header, secret): expected_signature = hmac.new( secret.encode('utf-8'), raw_body, hashlib.sha256 ).hexdigest() return hmac.compare_digest(expected_signature, signature_header) @app.route('/webhooks/agree', methods=['POST']) def handle_webhook(): signature = request.headers.get('X-Webhook-Signature') if not verify_webhook_signature(request.data, signature, AGREE_WEBHOOK_SECRET): return 'Invalid signature', 401 event = request.json if event['event'] == 'invoice.paid': # Handle successful payment pass elif event['event'] == 'invoice.failed': # Handle failed payment pass return 'OK', 200 ``` ## Managing Endpoints ### List All Endpoints ```bash curl https://api.agree.com/api/v1/webhook_endpoints \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### Update an Endpoint Change the subscribed events or URL: ```bash curl -X PUT https://api.agree.com/api/v1/webhook_endpoints/550e8400-e29b-41d4-a716-446655440000 \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "webhook_endpoint": { "events": ["invoice.paid", "invoice.failed", "invoice.created"] } }' ``` ### Disable an Endpoint Set `active` to false to temporarily stop receiving webhooks: ```bash curl -X PUT https://api.agree.com/api/v1/webhook_endpoints/550e8400-e29b-41d4-a716-446655440000 \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "webhook_endpoint": { "active": false } }' ``` ### Delete an Endpoint ```bash curl -X DELETE https://api.agree.com/api/v1/webhook_endpoints/550e8400-e29b-41d4-a716-446655440000 \ -H "Authorization: Bearer YOUR_API_KEY" ``` ## Retry Policy If your endpoint returns a non-2xx status code or times out, Agree automatically retries: | Attempt | Delay | |---------|-------| | 1 | Immediate | | 2 | ~1 minute | | 3 | ~5 minutes | | 4 | ~30 minutes | | 5 | ~2 hours | After 5 failed attempts, the webhook is marked as failed and the endpoint's `failure_count` is incremented. **Tip:** Monitor `failure_count` to detect integration issues. If it keeps increasing, check your endpoint's logs. ## Best Practices 1. **Respond quickly** - Return 200 within 5 seconds, then process asynchronously 2. **Handle duplicates** - Webhooks may occasionally be sent more than once; use idempotency 3. **Verify signatures** - Never skip verification in production 4. **Use HTTPS** - Required in production for security 5. **Log everything** - Store webhook payloads for debugging and auditing ## Fields Reference | Field | Type | Description | |-------|------|-------------| | `id` | UUID | Unique endpoint identifier | | `url` | string | Your webhook URL (HTTPS required in production) | | `events` | array | Event types this endpoint receives | | `active` | boolean | Whether the endpoint is receiving webhooks | | `failure_count` | integer | Consecutive failed deliveries | | `secret` | string | Signing secret (only returned on creation) | | `inserted_at` | datetime | When the endpoint was created | | `updated_at` | datetime | When the endpoint was last modified |