Skip to main content

Use webhooks to get real-time updates

Quaderno uses webhooks to notify your application when an event occurs in your account. Webhooks are particularly useful for asynchronous events, such as when an invoice is issued, a customer successfully completes a checkout, a tax report is generated, or you have surpassed a threshold in a new jurisdiction.

Quaderno uses HTTPS to send real-time notifications to your app as a JSON payload. You can use these notifications to execute actions in your backend systems.

This guide covers event types, errors and webhook signatures, as well as some payload examples. For creating, updating, retrieving, listing or deleting webhook objects, please refer to the Webhooks API reference.

Create a webhook endpoint

To create a webhook object in Quaderno, you must first create a webhook endpoint in your backend.

Creating a webhook endpoint is no different from creating any other page on your website. It's a simple HTTPS endpoint on your server with a URL. Quaderno will validate the provided URL with an initial HEAD request which must return a 200 OK HTTP status code.

Your endpoint must be configured to read event objects for the type of event notifications you want to receive. You can use one endpoint to handle several different event types at once or set up individual endpoints for specific events. Quaderno sends events to your webhook endpoints as part of a POST request with a JSON payload.

Once you have set up your endpoint, you can create the webhook on Quaderno either through your dashboard or by using our Webhooks API.

Events

All Quaderno events uses the same data format, regardless of event type:

ParameterTypeDescription
event_typestringThe event which triggered the webhook (invoice.created, contact.deleted, payment.created, etc).
account_idintegerID of the account that originated the event.
datahashA simplified JSON representation of the object.

The available events in Quaderno are:

  • account.updated – Occurs whenever an account status or property has changed.
  • account.application.deauthorized – Occurs whenever a user deauthorizes an application. Sent to the related application only.
  • checkout.succeeded – Occurs when a checkout session has been successfully completed.
  • checkout.failed – Occurs when a checkout session fails.
  • checkout.abandoned – Occurs when a checkout session is abandoned by the customer.
  • contact.created – Occurs whenever a new contact is created.
  • contact.updated – Occurs whenever any property of a contact changes.
  • contact.deleted – Occurs whenever a contact is deleted.
  • credit.created – Occurs whenever a new credit note is created.
  • credit.updated – Occurs whenever a credit note changes (e.g., the credit amount).
  • expense.created – Occurs whenever a new expense is created.
  • expense.updated – Occurs whenever an expense changes (e.g., the expense amount).
  • expense.deleted – Occurs whenever an expense is deleted.
  • invoice.created – Occurs whenever a new invoice is created. Note that this event is sent before payments are recorded. Subscribe to payment.created to get payments info.
  • invoice.updated – Occurs whenever an invoice changes (e.g., the invoice amount).
  • payment.created – Occurs whenever a new payment is created.
  • payment.deleted – Occurs whenever a payment is deleted.
  • reporting.request.succeeded – Occurs whenever a requested report completed succesfully.
  • reporting.request.failed – Occurs whenever a requested report failed to complete.
  • threshold.warning - Occurs whenever a tax threshold is about to be reached. Useful for contacting the tax agency where you'll need to start filing taxes soon (or your tax advisor).
  • threshold.exceeded - Occurs whenever a tax threshold is reached. Useful for registering on the corresponding jurisdiction.
  • threshold.eu.100k - Occurs whenever sales of digital services within the EU (exclusive of VAT) reach €100,000. Applies only to EU-based sellers.

Errors and retry policy

When retrieving webhooks, the responses contains a field called last_error.

When your event-receiving endpoint does not respond with a 200 when Quaderno POSTs the data, Quaderno will try again within 48 hours.

Failing to respond a second time will remove your subscription to that event.

Securing your webhooks

Quaderno signs all webhook events it sends to your endpoints by including a signature in each event’s X-Quaderno-Signature header. This allows you to verify that requests were sent by Quaderno and not by a third-party impersonating us.

The key is returned when you create a webhook or when you list all your webhooks.

To verify a request, you must generate a signature using the same secret key used by Quaderno and comparing the result to the value of the X-Quaderno-Signature header.

In your code that processes webhooks:

  1. Create a string with the webhook's URL, exactly as you entered it in Quaderno (including any query string, if applicable). Quaderno always signs webhook requests with the exact URL you provided when you configured the webhook. A difference as small as including or removing a trailing slash will prevent the signature from validating.
  2. Append the POST body to the URL string, with no delimiter. The body should be a JSON-encoded representation of the data.
  3. Hash the resulting string with HMAC-SHA1, using your webhook's authentication key to generate a binary signature.
  4. Base64 encode the binary signature.
  5. Compare the binary signature that you generated to the signature provided in the X-Quaderno-Signature HTTP header.

Payload examples

Here are some examples of webhook payloads generated by Quaderno.

invoice.created payload
{
"event_type": "invoice.created",
"account_id": 61,
"data": {
"object": {
"evidence": "confirmed",
"id": 154819,
"contact_id": 229899,
"tag_list": [],
"number": "00023",
"issue_date": "2023-06-09",
"contact_name": "Alex Wick",
"currency": "GBP",
"gross_amount_cents": 825,
"total_cents": 990,
"amount_paid_cents": 0,
"po_number": null,
"payment_details": null,
"notes": null,
"state": "outstanding",
"subject": null,
"street_line_1": "67 Church Lane",
"street_line_2": null,
"city": "London",
"postal_code": "E94 7RT",
"region": null,
"country": "GB",
"tax_id": null,
"processor_id": null,
"processor": null,
"processor_fee_cents": null,
"custom_metadata": {},
"due_date": null,
"permalink": "63ebdcb3c98ee5f7daa6d5eb519f",
"contact": {
"first_name": "Alex",
"last_name": "Wick",
"email": null,
"contact_person": null
},
"gross_amount": "8.25",
"total": "9.90",
"amount_paid": "0.00",
"exchange_rate": "1.161265",
"items": [
{
"id": 225369,
"product_code": null,
"description": "Simple Software",
"quantity": "1.0",
"unit_price": "8.25",
"discount_rate": "0.0",
"tax_1_amount_cents": 165,
"tax_1_name": null,
"tax_1_rate": 20,
"tax_1_country": "GB",
"tax_2_amount_cents": 0,
"tax_2_name": null,
"tax_2_rate": null,
"tax_2_country": "GB",
"subtotal_cents": "825.0",
"total_amount_cents": 990,
"discount_cents": "0.0",
"taxes_included": true,
"reference": null,
"tax_1_amount": "1.65",
"tax_2_amount": "0.00",
"unit_price_cents": "825.00",
"discount": "0.00",
"subtotal": "8.25",
"total_amount": "9.90"
}
],
// invoice.created is sent before payments are recorded
// subscribe to payment.created to get payments info
"payments": []
}
}
}
contact.updated payload
{
"event_type": "contact.updated",
"account_id": 61,
"data": {
"object": {
"id": 228249,
"kind": "person",
"first_name": "John",
"last_name": "Wick 4",
"full_name": "John Wick 4",
"contact_name": null,
"street_line_1": "67 Church Lane",
"street_line_2": null,
"postal_code": "E94 7RT",
"city": "London",
"region": null,
"country": "GB",
"phone_1": null,
"email": null,
"web": null,
"discount": null,
"language": "EN",
"tax_id": null,
"bank_account": null,
"notes": null,
"bic": null,
"vat_number": null,
"currency": "GBP",
"processor_id": null,
"processor": null
}
}
}
payment.created payload
{
"event_type": "payment.created",
"account_id": 61,
"data": {
"object": {
"id": 110847,
"document_id": 154835,
"date": "2023-06-09",
"payment_method": "credit_card",
"amount_cents": 990,
"processor": "apaymentprocessor",
"processor_id": "12345",
"amount": "9.90"
}
}
}
checkout.succeeded payload
{
"event_type":"checkout.succeeded",
"account_id": 99999,
"data":{
"object":{
"transaction_details":{
"session":43,
"session_permalink":"https://demo.quadernoapp.com/checkout/session/8ccf3fdc42b85800188b094b3",
"gateway":"stripe",
"type":"charge",
"description":"Unicorn",
"customer":"cus_FXesSy8Oz",
"email":"john@doe.com",
"transaction":"pi_1F2ZYtEjVKlcq2as8H5V",
"product_id":"prod_61ffa84a0b8",
"tax_name":"VAT",
"tax_rate":20.0,
"extra_tax_name":null,
"extra_tax_rate":null,
"iat":15647784,
"amount_cents":1500,
"amount":"15.00",
"currency":"EUR"
},
"contact":{
"id":547540,
"kind":"company",
"first_name":"John ",
"last_name":"Doe",
"full_name":"John Doe",
"contact_name":null,
"street_line_1":"Fake Street 1",
"postal_code":"SW15 5PU",
"city":null,
"region":null,
"country":"GB",
"email":"john@doe.com",
"web":null,
"language":"EN",
"tax_id":null,
}
}
}
}
checkout.failed payload
{
"event_type":"checkout.failed",
"account_id": 99999,
"data":{
"object":{
"message":{
"response_message":"Insufficient funds",
"status_code":422
},
"transaction_details":{
"gateway":"stripe",
"type":"charge",
"description":"Unicorn",
},
"contact":{
"id":547540,
"kind":"company",
"first_name":"John ",
"last_name":"Doe",
"full_name":"John Doe",
"contact_name":null,
"street_line_1":"Fake Street 1",
"postal_code":"SW15 5PU",
"city":null,
"region":null,
"country":"GB",
"email":"john@doe.com",
"web":null,
"language":"EN",
"tax_id":null,
}
}
}
}
checkout.abandoned payload
{
"event_type":"checkout.abandoned",
"account_id": 99999,
"data":{
"object":{
"transaction_details":{
"description":"Unicorn",
"plan":"awesome"
},
"contact":{
"first_name":"John ",
"last_name":"Doe",
"city":null,
"country":"GB",
"email":"john@doe.com"
}
}
}
}
threshold.warning payload
{
"event_type": "threshold.warning",
"account_id": 20988,
"data": {
"object": {
"name": "90%",
"country": "AU",
"region": null,
"postal_code": null,
"date": "2023-10-08T08:43:48.000Z"
}
}
}