Webhooks Setup Guide
Welcome to the Wave Webhooks guide! Whether you're building a commercial application for Wave users or looking to automate your own workflow, this guide will walk you through the end-to-end setup for registering and listening for Webhooks.
⚠️ Before You Begin
- Anyone can create an app and configure webhooks, but authorization for receiving webhooks requires a Pro account.
-
Your Endpoint: You must have a publicly accessible URL (e.g., https://yourdomain.com/webhook-receiver) ready to receive POST requests.
-
Additional requirements:
- Your endpoint must support
httpsand use a valid, CA-signed certificate (not self-signed). It should support TLS 1.2 or higher. - It cannot point to private or internal networks.
- Your endpoint must support
-
Additional requirements:
Create Your Application
Before you can set up a webhook, you need to create an application.
- Log in to your Wave account.
- Navigate to Manage Applications.
- Click + Create New Application or an existing application.
- Enter your Application Name and Redirect URI (this is used for the OAuth flow later).
- Save your Client ID and Client Secret somewhere secure.
OAuth Scope Checking
Just because a webhook subscription is active doesn't mean data is flowing yet. Your app's users must authorize the application to access the appropriate business data.
Required OAuth Scopes
It is mandatory to include the correct scopes in the OAuth flow that your users go through in order to successfully receive their webhook events.
If the necessary scopes are missing, event delivery will be skipped for the users that did not grant the required permissions. You can reference the following mapping of all of our supported events to the scopes they require:
| Event types | Minimum scopes required |
|
invoice:read OR invoice:* |
|
estimate:read OR estimate:* |
If your app was not requesting the necessary scopes, or if you start listening to new events that require additional scopes not previously requested, you must:
- Update your OAuth code to request the additional scopes,
- Contact your users and ask them to manually re-authorize their connection to your application to enable the new permissions.
You can learn more about all our supported scopes in the OAuth Scopes article. If you have any questions, please contact our developer support team by opening a request here.
Configure Webhook Settings
Once your app is created, it’s time to tell us where to send the data.
- Navigate to the Webhooks page.
- Select your newly created app. If it’s your first time, the status will show Needs Configuration.
- Endpoint URL: Paste the destination URL where your server is listening.
-
Select Trigger Events: In the “Listening” tab under Events list, check the boxes for the data you want to receive.
- More supported events will be available soon!
Payload Examples
Click each event below to expand its example payload.
Invoices
invoice.overdue event
{
"event_id": "test-event-id",
"event_type": "invoice.overdue",
"business_id": "test-business-id",
"data": {
"invoice_id": "123",
"customer_id": "123",
"currency_code": "USD",
"due_date": "2026-03-30",
"invoice_balance": "200.00",
"issue_date": "2026-03-30"
}
}invoice.viewed event
{
"event_id": "test-event-id",
"event_type": "invoice.viewed",
"business_id": "test-business-id",
"data": {
"invoice_id": "123",
"customer_id": "123",
"currency_code": "USD",
"view_timestamp": "2026-03-30T06:18:01.212000+00:00",
"invoice_balance": "200.00"
}
}invoice.approved event
{
"event_id": "test-event-id",
"event_type": "invoice.approved",
"business_id": "test-business-id",
"data": {
"invoice_id": "123",
"customer_id": "123",
"currency_code": "USD",
"amount": "200.00",
"issue_date": "2026-03-30"
"due_date": "2026-03-30",
}
}invoice.paid event
{
"business_id": "63c77f29-875c-4cdc-947d-07430f628043",
"data": {
"amount_paid": "8.98",
"currency_code": "USD",
"customer_id": "21840161",
"invoice_id": "2496756670638588934",
"paid_date": "2026-04-29",
"remaining_balance": "0.00"
},
"event_id": "2f210c44-f1ab-551e-89fa-333fb8d2a5fe",
"event_type": "invoice.paid"
}invoice.overpaid event
{
"business_id": "63c77f29-875c-4cdc-947d-07430f628043",
"data": {
"amount_paid": "50.22",
"currency_code": "USD",
"customer_id": "21840161",
"invoice_id": "2496760399374844951",
"paid_date": "2026-04-29",
"remaining_balance": "-10.00"
},
"event_id": "3783d1d5-d9e6-5ade-8d41-97da0029cf0f",
"event_type": "invoice.overpaid"
}invoice.partially_paid event
{
"business_id": "63c77f29-875c-4cdc-947d-07430f628043",
"data": {
"amount_paid": "10.22",
"currency_code": "USD",
"customer_id": "21840161",
"invoice_id": "2496760399374844951",
"paid_date": "2026-04-29",
"remaining_balance": "30.00"
},
"event_id": "a704a98d-2e0a-5b5e-894a-0447fd56c5fb",
"event_type": "invoice.partially_paid"
}invoice.sent event
{
"business_id": "63c77f29-875c-4cdc-947d-07430f628043",
"data": {
"currency_code": "USD",
"customer_id": "21840161",
"invoice_id": "2496756670638588934",
"sent_at": "2026-04-29T16:58:36.818000+00:00",
"sent_method": "WAVE"
},
"event_id": "f670008d-ba99-5c77-8b5f-1294f06115ba",
"event_type": "invoice.sent"
}Estimates
estimate.approved event
{
"business_id": "63c77f29-875c-4cdc-947d-07430f628043",
"data": {
"estimate_id": "123",
"customer_id": "21840161",
"currency_code": "USD",
"status": "approved",
"amount": "200.00",
"estimate_date": "2026-03-30",
"due_date": "2026-04-30",
"deposit_amount_paid": "0.00",
"deposit_payment_status": null
},
"event_id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"event_type": "estimate.approved"
}estimate.accepted event
{
"business_id": "63c77f29-875c-4cdc-947d-07430f628043",
"data": {
"estimate_id": "123",
"customer_id": "21840161",
"currency_code": "USD",
"status": "accepted",
"accepted_at": "2026-03-30T06:18:01.212000+00:00",
"due_date": "2026-04-30",
"amount": "200.00"
},
"event_id": "2b3c4d5e-6f7a-8b9c-0d1e-2f3a4b5c6d7e",
"event_type": "estimate.accepted"
}estimate.sent event
{
"business_id": "63c77f29-875c-4cdc-947d-07430f628043",
"data": {
"estimate_id": "123",
"customer_id": "21840161",
"currency_code": "USD",
"status": "sent",
"sent_at": "2026-03-30T06:18:01.212000+00:00",
"due_date": "2026-04-30",
"amount": "200.00"
},
"event_id": "3c4d5e6f-7a8b-9c0d-1e2f-3a4b5c6d7e8f",
"event_type": "estimate.sent"
}estimate.viewed event
{
"business_id": "63c77f29-875c-4cdc-947d-07430f628043",
"data": {
"estimate_id": "123",
"customer_id": "21840161",
"currency_code": "USD",
"status": "viewed",
"view_timestamp": "2026-03-30T06:18:01.212000+00:00",
"due_date": "2026-04-30",
"amount": "200.00"
},
"event_id": "4d5e6f7a-8b9c-0d1e-2f3a-4b5c6d7e8f90",
"event_type": "estimate.viewed"
}estimate.reset event
{
"business_id": "63c77f29-875c-4cdc-947d-07430f628043",
"data": {
"estimate_id": "123",
"customer_id": "21840161",
"currency_code": "USD",
"status": "APPROVED",
"reset_at": "2026-03-30T06:18:01.212000+00:00",
"due_date": "2026-04-30",
"amount": "200.00"
},
"event_id": "5e6f7a8b-9c0d-1e2f-3a4b-5c6d7e8f9001",
"event_type": "estimate.reset"
}estimate.expired event
{
"business_id": "63c77f29-875c-4cdc-947d-07430f628043",
"data": {
"estimate_id": "123",
"customer_id": "21840161",
"currency_code": "USD",
"status": "expired",
"expired_at": "2026-03-30T06:18:01.212000+00:00",
"due_date": "2026-04-30",
"amount": "200.00"
},
"event_id": "6f7a8b9c-0d1e-2f3a-4b5c-6d7e8f900112",
"event_type": "estimate.expired"
}estimate.paid event
{
"business_id": "63c77f29-875c-4cdc-947d-07430f628043",
"data": {
"estimate_id": "123",
"customer_id": "21840161",
"currency_code": "USD",
"status": "ACCEPTED",
"paid_date": "2026-04-29",
"due_date": "2026-04-30",
"estimate_total_amount": "500.00",
"deposit_amount_paid": "100.00",
"remaining_deposit_balance": "0.00"
},
"event_id": "7a8b9c0d-1e2f-3a4b-5c6d-7e8f90011223",
"event_type": "estimate.paid"
}
If needed, you can calculate the total deposit required and the remaining estimate balance using the fields provided in the data object:
-
Total Deposit Amount:
total_deposit = deposit_amount_paid + remaining_deposit_balance -
Remaining Estimate Balance:
remaining_estimate_balance = estimate_total_amount - deposit_amount_paid
estimate.partially_paid event
{
"business_id": "63c77f29-875c-4cdc-947d-07430f628043",
"data": {
"estimate_id": "123",
"customer_id": "21840161",
"currency_code": "USD",
"status": "ACCEPTED",
"paid_date": "2026-04-29",
"due_date": "2026-04-30",
"estimate_total_amount": "500.00",
"deposit_amount_paid": "30.00",
"remaining_deposit_balance": "70.00"
},
"event_id": "8b9c0d1e-2f3a-4b5c-6d7e-8f9001122334",
"event_type": "estimate.partially_paid"
}
If needed, you can calculate the total deposit required and the remaining estimate balance using the fields provided in the data object:
-
Total Deposit Amount:
total_deposit = deposit_amount_paid + remaining_deposit_balance -
Remaining Estimate Balance:
remaining_estimate_balance = estimate_total_amount - deposit_amount_paid
estimate.overpaid event
{
"business_id": "63c77f29-875c-4cdc-947d-07430f628043",
"data": {
"estimate_id": "123",
"customer_id": "21840161",
"currency_code": "USD",
"status": "ACCEPTED",
"paid_date": "2026-04-29",
"due_date": "2026-04-30",
"estimate_total_amount": "500.00",
"deposit_amount_paid": "110.00",
"remaining_deposit_balance": "-10.00"
},
"event_id": "9c0d1e2f-3a4b-5c6d-7e8f-900112233445",
"event_type": "estimate.overpaid"
}
If needed, you can calculate the total deposit required and the remaining estimate balance using the fields provided in the data object:
-
Total Deposit Amount:
total_deposit = deposit_amount_paid + remaining_deposit_balance -
Remaining Estimate Balance:
remaining_estimate_balance = estimate_total_amount - deposit_amount_paid
Note: In an overpaid state, the remaining estimate balance and remaining deposit balance will typically result in a negative number, representing the credit amount overpaid by the customer.
Wave Webhook Signature Verification
All Wave webhooks include two security headers:
-
x-wave-signature(format:t=timestamp,v1=signature) and -
x-wave-timestamp(the timestamp as a string).
Webhook Verification Steps:
- Extract the timestamp and signature from the x-wave-signature header.
- Recreate the signed payload: {timestamp}.{raw_request_body}
- Generate an HMAC-SHA256 signature using your webhook secret as the key.
- Compare your generated signature with the received one.
- Ensure the timestamp is within 5 minutes to prevent replay attacks.
Note: Use the raw request body as-is. Do not parse and re-serialize the JSON — even a small formatting difference (e.g. whitespace, key ordering) will cause verification to fail.
This ensures the webhook genuinely came from Wave and hasn't been tampered with.
Code example (in Python):
import hashlib
import hmac
import time
def verify_wave_webhook(x_wave_signature: str, raw_body, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in x_wave_signature.split(","))
timestamp = parts["t"]
received_sig = parts["v1"]
if abs(int(time.time()) - int(timestamp)) > 300:
raise ValueError("Webhook timestamp outside the 5-minute tolerance window")
if isinstance(raw_body, bytes):
raw_body = raw_body.decode("utf-8")
signed_payload = f"{timestamp}.{raw_body}"
computed_sig = hmac.new(
secret.encode("utf-8"),
signed_payload.encode("utf-8"),
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(computed_sig, received_sig)
Encoding IDs for API Requests
The IDs provided in the event payload (e.g., business_id, invoice_id, estimate_id, customer_id) are raw UUIDs. When using these as arguments in other Public API endpoints, they must be formatted as a prefixed string and then Base64 encoded.
Encoding Logic
To prepare an ID for an API call, follow this pattern:
- Format the string: Combine the resource prefixes and raw IDs.
- Add the separator: Use a semicolon (;) to separate multiple entities if encoding an Invoice, Estimate or Customer (they require the associated business ID in addition to the entity ID)
- Encode: Convert the entire string to Base64.
Formatting Guide
| Resource Type | Plaintext Format |
| Business | Business:{business_id} |
| Invoice | Business:{business_id};Invoice:{invoice_id} |
| Estimate | Business:{business_id};Estimate:{estimate_id} |
| Customer | Business:{business_id};Customer:{customer_id} |
Step-by-Step Example (Business ID)
Example:
- Raw ID: test-business-id
- String to encode: Business:test-business-id
- Final encoded ID: QnVzaW5lc3M6dGVzdC1idXNpbmVzcy1pZA==
Step-by-Step Example (Invoice ID)
If you are calling an endpoint that requires invoiceId:
- Raw Business ID: test-business-id
- Raw Invoice ID: test-invoice-id
- Combined String: Business:test-business-id;Invoice:test-invoice-id
- Final Base64 Value: QnVzaW5lc3M6dGVzdC1idXNpbmVzcy1pZDtJbnZvaWNlOnRlc3QtaW52b2ljZS1pZA==
Step-by-Step Example (Estimate ID)
If you are calling an endpoint that requires estimateId:
- Raw Business ID: test-business-id
- Raw Estimate ID: test-estimate-id
- Combined String: Business:test-business-id;Estimate:test-estimate-id
- Final Base64 Value: QnVzaW5lc3M6dGVzdC1idXNpbmVzcy1pZDtFc3RpbWF0ZTp0ZXN0LWVzdGltYXRlLWlk
Managing & Testing
On the Webhooks dashboard, you can monitor the health of your integration:
| Feature | Description |
| Logs & deliveries |
Navigate to the Logs & deliveries section within the Events List to inspect the JSON payload dispatched to your configured endpoint for every event, alongside the HTTP response received from your server for verification purposes. Expanding any specific entry reveals a Re-trigger event option, allowing you to manually resubmit that particular request payload to your endpoint. You can utilize the available filters to sort logs by Status (successes or failures) or by a unique Business ID. When searching by business, input the raw “business_id” UUID found in the payload, as this filter does not require Base64 encoding. |
| Status Toggle | Easily Pause or Resume a webhook without deleting the configuration. |
| Manage App | A quick link to jump back to your Client ID/Secret settings. |
Retry Mechanism
Wave uses an exponential backoff strategy to automatically retry webhook deliveries that fail due to transient errors. Transient failures include:
- 5xx status codes
- Network timeouts
- Connection errors
These are retried to give your system time to recover from temporary outages.
When a transient failure occurs, the system attempts delivery up to 6 times over an approximate 14-hour window, using exponential backoff to progressively increase the delay between attempts. This method ensures your system is not overwhelmed during recovery. The sequence of delays is:
- ~1 minute (Attempt 2),
- ~5 minutes (Attempt 3),
- ~30 minutes (Attempt 4),
- ~2 hours (Attempt 5), and
- ~12 hours (Attempt 6).
If the delivery still fails after the 6th attempt, no further retries are performed, and the message is routed to a Dead Letter Queue (DLQ) for manual investigation by the Wave team.
Please note, however, that permanent failures, indicated by 4xx status codes (such as 400, 401, or 404), are not retried. These responses suggest a misconfiguration on your endpoint's side (e.g., wrong URL or invalid authentication), and retrying would not resolve the issue. In the case of a 4xx error, the delivery is immediately marked as permanently failed, and its request and response payloads can be found in the “Logs & deliveries” tab for your own investigation.
Troubleshooting
- Why am I not receiving data? Check if the business being connected has an active Pro subscription. Standard/Starter businesses do not support webhook delivery. You are only able to OAuth to an application with a Pro enabled business. Additionally, ensure that you are requesting the correct OAuth scopes for the events you are listening to. You can read more about this topic in the OAuth Scope Checking section of this guide.
- Invoice Viewed Events: For the “Invoice Viewed” event, specifically, this event is not triggered if you view an invoice while logged into the account/business that created the invoice
- Estimate Viewed Events: For the “Estimate Viewed” event, specifically, this event is not triggered if you view an estimate while logged into the account/business that created the estimate.