Webhook subscriptions
Subscriptions that push events to your server instead of you polling for them. A subscription belongs to the token that created it and only ever delivers events that token has the scope to read. The envelope, signature verification and the retry schedule are in Webhooks - read that first; this page is the subscription object and the event catalogue.
The object#
| Field | Type | Description |
|---|---|---|
| idread-only | integer | NolaPro id. |
| url | string(500) | HTTPS endpoint to POST to. Plain HTTP is refused - the payload carries business data and the signature is worthless over a channel anyone can read. |
| events | array of string | Event types to receive. * is not accepted: naming them is what stops a new event type silently appearing in your handler. |
| description | string(100) | What this subscription is for. Shown on the token screen. |
| paused | boolean | Pause delivery without cancelling the subscription. A paused subscription drops events rather than queueing them - it is a tap, not a buffer. Deliberately not called active: D23 reserves that word, and cancel already means something else here. |
| failcountread-only | integer | Consecutive failed deliveries. Resets on the first success. |
| lastdeliverydateread-only | string | Last successful delivery. |
| disableddateread-only | string | Set when we gave up. See the disabling rule in Webhooks. |
| cancelread-only | boolean | Cancelled. |
| enabledrequest-only | boolean | Write-only. Send true to revive a subscription we disabled after six failed deliveries: it clears disableddate AND resets failcount, so it gets a fresh six. Clearing the date alone would disable it again on the next failure. Not the same as paused, which is your own on/off switch and never resets anything. |
Endpoints#
List subscriptions.
Requires scope setup:read.
| Name | In | Type | Notes |
|---|---|---|---|
| cancel | query | string | Defaults to false. Pass true or any. |
curl \ 'https://acme.nolapro.com/!/api/v2/webhooks' \ -H 'Authorization: Bearer $NP_TOKEN'
$ch = curl_init('https://acme.nolapro.com/!/api/v2/webhooks'); curl_setopt_array($ch, [ CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('NP_TOKEN')], CURLOPT_RETURNTRANSFER => true, ]); $res = json_decode(curl_exec($ch), true);
import os, requests r = requests.get( "https://acme.nolapro.com/!/api/v2/webhooks", headers={"Authorization": f"Bearer {os.environ['NP_TOKEN']}"}, ) r.raise_for_status()
const res = await fetch( 'https://acme.nolapro.com/!/api/v2/webhooks', { method: 'GET', headers: { Authorization: `Bearer ${process.env.NP_TOKEN}` }, } ); const data = await res.json();
using System.Net.Http.Json; var token = Environment.GetEnvironmentVariable("NP_TOKEN"); using var http = new HttpClient(); http.DefaultRequestHeaders.Add("Authorization", "Bearer " + token); var res = await http.GetAsync( "https://acme.nolapro.com/!/api/v2/webhooks"); res.EnsureSuccessStatusCode();
Create a subscription.
The response is the only time secret is returned. Store it before you acknowledge the call.
Requires scope setup:write.
| Name | In | Type | Notes |
|---|---|---|---|
| Field | Type | Description |
|---|---|---|
| idread-only | integer | NolaPro id. |
| url | string(500) | HTTPS endpoint to POST to. Plain HTTP is refused - the payload carries business data and the signature is worthless over a channel anyone can read. |
| events | array of string | Event types to receive. * is not accepted: naming them is what stops a new event type silently appearing in your handler. |
| description | string(100) | What this subscription is for. Shown on the token screen. |
| paused | boolean | Pause delivery without cancelling the subscription. A paused subscription drops events rather than queueing them - it is a tap, not a buffer. Deliberately not called active: D23 reserves that word, and cancel already means something else here. |
| failcountread-only | integer | Consecutive failed deliveries. Resets on the first success. |
| lastdeliverydateread-only | string | Last successful delivery. |
| disableddateread-only | string | Set when we gave up. See the disabling rule in Webhooks. |
| cancelread-only | boolean | Cancelled. |
| enabledrequest-only | boolean | Write-only. Send true to revive a subscription we disabled after six failed deliveries: it clears disableddate AND resets failcount, so it gets a fresh six. Clearing the date alone would disable it again on the next failure. Not the same as paused, which is your own on/off switch and never resets anything. |
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | A required field was missing or malformed. |
| 422 | unsupported_value | The url is not https, so the payload and its signature would travel in clear. |
| 422 | unknown_value | An event name is not in the catalogue. |
curl -X POST \ 'https://acme.nolapro.com/!/api/v2/webhooks' \ -H 'Authorization: Bearer $NP_TOKEN' \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: your-unique-key' \ -d '{ "url": "Example url", "description": "Example description", "paused": false, "enabled": false }'
$ch = curl_init('https://acme.nolapro.com/!/api/v2/webhooks'); curl_setopt_array($ch, [ CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('NP_TOKEN')], CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => 'POST', CURLOPT_POSTFIELDS => json_encode([ 'url' => 'Example url', 'description' => 'Example description', 'paused' => false, 'enabled' => false, ]), ]); $res = json_decode(curl_exec($ch), true);
import os, requests r = requests.post( "https://acme.nolapro.com/!/api/v2/webhooks", headers={"Authorization": f"Bearer {os.environ['NP_TOKEN']}"}, json={ "url": "Example url", "description": "Example description", "paused": False, "enabled": False, }, ) r.raise_for_status()
const res = await fetch( 'https://acme.nolapro.com/!/api/v2/webhooks', { method: 'POST', headers: { Authorization: `Bearer ${process.env.NP_TOKEN}` }, body: JSON.stringify({ url: "Example url", description: "Example description", paused: false, enabled: false, }), } ); const data = await res.json();
using System.Net.Http.Json; var token = Environment.GetEnvironmentVariable("NP_TOKEN"); using var http = new HttpClient(); http.DefaultRequestHeaders.Add("Authorization", "Bearer " + token); var body = new { url = "Example url", description = "Example description", paused = false, enabled = false, }; var req = new HttpRequestMessage(HttpMethod.Post, "https://acme.nolapro.com/!/api/v2/webhooks") { Content = JsonContent.Create(body), }; req.Headers.Add("Idempotency-Key", "your-unique-key"); var res = await http.SendAsync(req); res.EnsureSuccessStatusCode();
{
"url": "Example url",
"description": "Example description",
"paused": false,
"secret": "Example secret",
"secret_note": "Example secret note"
}
Retrieve one subscription.
Requires scope setup:read.
| Name | In | Type | Notes |
|---|---|---|---|
| idrequired | path | integer |
| Status | Code | Meaning |
|---|---|---|
| 404 | not_found | No subscription with that id. |
curl \ 'https://acme.nolapro.com/!/api/v2/webhooks/104' \ -H 'Authorization: Bearer $NP_TOKEN'
$ch = curl_init('https://acme.nolapro.com/!/api/v2/webhooks/104'); curl_setopt_array($ch, [ CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('NP_TOKEN')], CURLOPT_RETURNTRANSFER => true, ]); $res = json_decode(curl_exec($ch), true);
import os, requests r = requests.get( "https://acme.nolapro.com/!/api/v2/webhooks/104", headers={"Authorization": f"Bearer {os.environ['NP_TOKEN']}"}, ) r.raise_for_status()
const res = await fetch( 'https://acme.nolapro.com/!/api/v2/webhooks/104', { method: 'GET', headers: { Authorization: `Bearer ${process.env.NP_TOKEN}` }, } ); const data = await res.json();
using System.Net.Http.Json; var token = Environment.GetEnvironmentVariable("NP_TOKEN"); using var http = new HttpClient(); http.DefaultRequestHeaders.Add("Authorization", "Bearer " + token); var res = await http.GetAsync( "https://acme.nolapro.com/!/api/v2/webhooks/104"); res.EnsureSuccessStatusCode();
{
"url": "Example url",
"description": "Example description",
"paused": false,
"enabled": false
}
Update a subscription.
Change the URL, the event list, or pause it. The secret cannot be read back; send a new one to rotate it.
Requires scope setup:write.
| Name | In | Type | Notes |
|---|---|---|---|
| idrequired | path | integer | |
| Field | Type | Description |
|---|---|---|
| idread-only | integer | NolaPro id. |
| url | string(500) | HTTPS endpoint to POST to. Plain HTTP is refused - the payload carries business data and the signature is worthless over a channel anyone can read. |
| events | array of string | Event types to receive. * is not accepted: naming them is what stops a new event type silently appearing in your handler. |
| description | string(100) | What this subscription is for. Shown on the token screen. |
| paused | boolean | Pause delivery without cancelling the subscription. A paused subscription drops events rather than queueing them - it is a tap, not a buffer. Deliberately not called active: D23 reserves that word, and cancel already means something else here. |
| failcountread-only | integer | Consecutive failed deliveries. Resets on the first success. |
| lastdeliverydateread-only | string | Last successful delivery. |
| disableddateread-only | string | Set when we gave up. See the disabling rule in Webhooks. |
| cancelread-only | boolean | Cancelled. |
| enabledrequest-only | boolean | Write-only. Send true to revive a subscription we disabled after six failed deliveries: it clears disableddate AND resets failcount, so it gets a fresh six. Clearing the date alone would disable it again on the next failure. Not the same as paused, which is your own on/off switch and never resets anything. |
| Status | Code | Meaning |
|---|---|---|
| 404 | not_found | No subscription with that id. |
| 400 | invalid_request | Send at least one field to change. |
| 400 | unknown_parameter | The secret cannot be changed; create a new subscription. |
| 422 | unsupported_value | The url is not https. |
| 422 | unknown_value | An event name is not in the catalogue. |
curl -X PATCH \ 'https://acme.nolapro.com/!/api/v2/webhooks/104' \ -H 'Authorization: Bearer $NP_TOKEN' \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: your-unique-key' \ -d '{ "url": "Example url", "description": "Example description", "paused": false, "enabled": false }'
$ch = curl_init('https://acme.nolapro.com/!/api/v2/webhooks/104'); curl_setopt_array($ch, [ CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('NP_TOKEN')], CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => 'PATCH', CURLOPT_POSTFIELDS => json_encode([ 'url' => 'Example url', 'description' => 'Example description', 'paused' => false, 'enabled' => false, ]), ]); $res = json_decode(curl_exec($ch), true);
import os, requests r = requests.patch( "https://acme.nolapro.com/!/api/v2/webhooks/104", headers={"Authorization": f"Bearer {os.environ['NP_TOKEN']}"}, json={ "url": "Example url", "description": "Example description", "paused": False, "enabled": False, }, ) r.raise_for_status()
const res = await fetch( 'https://acme.nolapro.com/!/api/v2/webhooks/104', { method: 'PATCH', headers: { Authorization: `Bearer ${process.env.NP_TOKEN}` }, body: JSON.stringify({ url: "Example url", description: "Example description", paused: false, enabled: false, }), } ); const data = await res.json();
using System.Net.Http.Json; var token = Environment.GetEnvironmentVariable("NP_TOKEN"); using var http = new HttpClient(); http.DefaultRequestHeaders.Add("Authorization", "Bearer " + token); var body = new { url = "Example url", description = "Example description", paused = false, enabled = false, }; var req = new HttpRequestMessage(HttpMethod.Patch, "https://acme.nolapro.com/!/api/v2/webhooks/104") { Content = JsonContent.Create(body), }; req.Headers.Add("Idempotency-Key", "your-unique-key"); var res = await http.SendAsync(req); res.EnsureSuccessStatusCode();
{
"url": "Example url",
"description": "Example description",
"paused": false,
"enabled": false
}
Cancel a subscription.
Requires scope setup:cancel.
| Name | In | Type | Notes |
|---|---|---|---|
| idrequired | path | integer | |
| Status | Code | Meaning |
|---|---|---|
| 404 | not_found | No subscription with that id. |
| 409 | cannot_cancel | Already cancelled. |
curl -X DELETE \ 'https://acme.nolapro.com/!/api/v2/webhooks/104' \ -H 'Authorization: Bearer $NP_TOKEN'
$ch = curl_init('https://acme.nolapro.com/!/api/v2/webhooks/104'); curl_setopt_array($ch, [ CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('NP_TOKEN')], CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => 'DELETE', ]); $res = json_decode(curl_exec($ch), true);
import os, requests r = requests.delete( "https://acme.nolapro.com/!/api/v2/webhooks/104", headers={"Authorization": f"Bearer {os.environ['NP_TOKEN']}"}, ) r.raise_for_status()
const res = await fetch( 'https://acme.nolapro.com/!/api/v2/webhooks/104', { method: 'DELETE', headers: { Authorization: `Bearer ${process.env.NP_TOKEN}` }, } ); const data = await res.json();
using System.Net.Http.Json; var token = Environment.GetEnvironmentVariable("NP_TOKEN"); using var http = new HttpClient(); http.DefaultRequestHeaders.Add("Authorization", "Bearer " + token); var req = new HttpRequestMessage(HttpMethod.Delete, "https://acme.nolapro.com/!/api/v2/webhooks/104"); var res = await http.SendAsync(req); res.EnsureSuccessStatusCode();
{
"url": "Example url",
"description": "Example description",
"paused": false,
"enabled": false
}
Send a test delivery.
Delivers a webhooks.test event to the subscription URL right now and returns what your endpoint answered, including the status code and the first 500 bytes of the body. Built because "it is not firing" is the most common webhook ticket and it is almost always a 500 on the receiving end that nobody could see.
Requires scope setup:write.
| Name | In | Type | Notes |
|---|---|---|---|
| idrequired | path | integer | |
| Status | Code | Meaning |
|---|---|---|
| 400 | unknown_parameter | The body carried a field this endpoint does not take. |
| 404 | not_found | The subscription went away between the lookup and the write. |
| 422 | unknown_value | The id did not resolve to a subscription this token can see. |
curl -X POST \ 'https://acme.nolapro.com/!/api/v2/webhooks/104/test' \ -H 'Authorization: Bearer $NP_TOKEN'
$ch = curl_init('https://acme.nolapro.com/!/api/v2/webhooks/104/test'); curl_setopt_array($ch, [ CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('NP_TOKEN')], CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => 'POST', ]); $res = json_decode(curl_exec($ch), true);
import os, requests r = requests.post( "https://acme.nolapro.com/!/api/v2/webhooks/104/test", headers={"Authorization": f"Bearer {os.environ['NP_TOKEN']}"}, ) r.raise_for_status()
const res = await fetch( 'https://acme.nolapro.com/!/api/v2/webhooks/104/test', { method: 'POST', headers: { Authorization: `Bearer ${process.env.NP_TOKEN}` }, } ); const data = await res.json();
using System.Net.Http.Json; var token = Environment.GetEnvironmentVariable("NP_TOKEN"); using var http = new HttpClient(); http.DefaultRequestHeaders.Add("Authorization", "Bearer " + token); var req = new HttpRequestMessage(HttpMethod.Post, "https://acme.nolapro.com/!/api/v2/webhooks/104/test"); var res = await http.SendAsync(req); res.EnsureSuccessStatusCode();
Deliver this subscription's queue now.
Runs the delivery lane for this subscription immediately instead of waiting for the next five-minute cycle, and clears any remaining retry wait first. That second part is the point. If your endpoint was down and you have just fixed it, the event at the head of the queue may be sitting out a retry gap, and everything behind it is waiting on that event. Flushing drops the wait and sends in order from wherever the queue stands. Order is preserved exactly as it is on a normal cycle: delivery stops at the first failure, and nothing behind a failing event is sent.
Requires scope setup:write.
| Name | In | Type | Notes |
|---|---|---|---|
| idrequired | path | integer | |
| Status | Code | Meaning |
|---|---|---|
| 400 | unknown_parameter | The body carried a field this endpoint does not take. |
| 404 | not_found | The subscription went away between the lookup and the write. |
| 422 | unknown_value | The id did not resolve to a subscription this token can see. |
curl -X POST \ 'https://acme.nolapro.com/!/api/v2/webhooks/104/flush' \ -H 'Authorization: Bearer $NP_TOKEN'
$ch = curl_init('https://acme.nolapro.com/!/api/v2/webhooks/104/flush'); curl_setopt_array($ch, [ CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('NP_TOKEN')], CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => 'POST', ]); $res = json_decode(curl_exec($ch), true);
import os, requests r = requests.post( "https://acme.nolapro.com/!/api/v2/webhooks/104/flush", headers={"Authorization": f"Bearer {os.environ['NP_TOKEN']}"}, ) r.raise_for_status()
const res = await fetch( 'https://acme.nolapro.com/!/api/v2/webhooks/104/flush', { method: 'POST', headers: { Authorization: `Bearer ${process.env.NP_TOKEN}` }, } ); const data = await res.json();
using System.Net.Http.Json; var token = Environment.GetEnvironmentVariable("NP_TOKEN"); using var http = new HttpClient(); http.DefaultRequestHeaders.Add("Authorization", "Bearer " + token); var req = new HttpRequestMessage(HttpMethod.Post, "https://acme.nolapro.com/!/api/v2/webhooks/104/flush"); var res = await http.SendAsync(req); res.EnsureSuccessStatusCode();
{
"sent": 4,
"failed": 0,
"pending": 0
}
What we tried to send, and what your endpoint said.
Every delivery attempt for this subscription, newest first, with the status code and the first 500 bytes your endpoint returned. This is the answer to "the webhook is not firing". It is almost always a 500 on the receiving end that never reached your own logs, and this shows it without anyone having to reproduce the problem. Delivered attempts are kept 7 days; ones that were dropped are kept 30 days, because a failure is what you need to look at later.
Requires scope setup:read.
| Name | In | Type | Notes |
|---|---|---|---|
| idrequired | path | integer | The subscription. |
| delivered | query | string | true for successes, false for everything still owed or dropped. |
| abandoned | query | string | true for events that ran out of attempts and were dropped. |
| eventtype | query | string | One event name, e.g. orders.created. |
| page | query | integer | |
| perpage | query | integer |
curl \ 'https://acme.nolapro.com/!/api/v2/webhooks/104/deliveries' \ -H 'Authorization: Bearer $NP_TOKEN'
$ch = curl_init('https://acme.nolapro.com/!/api/v2/webhooks/104/deliveries'); curl_setopt_array($ch, [ CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('NP_TOKEN')], CURLOPT_RETURNTRANSFER => true, ]); $res = json_decode(curl_exec($ch), true);
import os, requests r = requests.get( "https://acme.nolapro.com/!/api/v2/webhooks/104/deliveries", headers={"Authorization": f"Bearer {os.environ['NP_TOKEN']}"}, ) r.raise_for_status()
const res = await fetch( 'https://acme.nolapro.com/!/api/v2/webhooks/104/deliveries', { method: 'GET', headers: { Authorization: `Bearer ${process.env.NP_TOKEN}` }, } ); const data = await res.json();
using System.Net.Http.Json; var token = Environment.GetEnvironmentVariable("NP_TOKEN"); using var http = new HttpClient(); http.DefaultRequestHeaders.Add("Authorization", "Bearer " + token); var res = await http.GetAsync( "https://acme.nolapro.com/!/api/v2/webhooks/104/deliveries"); res.EnsureSuccessStatusCode();
Issue a new signing secret.
Mints a new secret and returns it once, the same rule as create.
Both secrets sign for 24 hours by default, and that is the point rather than a nicety. You cannot change your stored secret at the same instant we change ours, so a straight swap guarantees a window where every delivery fails your signature check - and an event that runs out of attempts is dropped, not delayed. During the overlap NP-Signature carries two v1 values, the old one first, so a receiver that reads only the first keeps working untouched until you deploy.
Send {"immediate": true} to skip the overlap if you can deploy the new secret atomically. Most cannot, which is why it is not the default.
Requires scope setup:write.
| Name | In | Type | Notes |
|---|---|---|---|
| idrequired | path | integer | |
| Field | Type | Description |
|---|---|---|
| immediaterequest-only | boolean | Retire the old secret at once instead of after 24 hours. |
| Status | Code | Meaning |
|---|---|---|
| 400 | unknown_parameter | The body carried a field this endpoint does not take. |
| 404 | not_found | The subscription went away between the lookup and the write. |
| 422 | unknown_value | The id did not resolve to a subscription this token can see. |
curl -X POST \ 'https://acme.nolapro.com/!/api/v2/webhooks/104/rotatesecret' \ -H 'Authorization: Bearer $NP_TOKEN'
$ch = curl_init('https://acme.nolapro.com/!/api/v2/webhooks/104/rotatesecret'); curl_setopt_array($ch, [ CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('NP_TOKEN')], CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => 'POST', ]); $res = json_decode(curl_exec($ch), true);
import os, requests r = requests.post( "https://acme.nolapro.com/!/api/v2/webhooks/104/rotatesecret", headers={"Authorization": f"Bearer {os.environ['NP_TOKEN']}"}, ) r.raise_for_status()
const res = await fetch( 'https://acme.nolapro.com/!/api/v2/webhooks/104/rotatesecret', { method: 'POST', headers: { Authorization: `Bearer ${process.env.NP_TOKEN}` }, } ); const data = await res.json();
using System.Net.Http.Json; var token = Environment.GetEnvironmentVariable("NP_TOKEN"); using var http = new HttpClient(); http.DefaultRequestHeaders.Add("Authorization", "Bearer " + token); var req = new HttpRequestMessage(HttpMethod.Post, "https://acme.nolapro.com/!/api/v2/webhooks/104/rotatesecret"); var res = await http.SendAsync(req); res.EnsureSuccessStatusCode();
[]
Events #
Every event NolaPro can send. All share the one envelope described in Webhooks; only type and data differ.
| Event | Scope | Fires when |
|---|---|---|
customers.created | customers:read | A customer was created. |
customers.updated | customers:read | A customer changed. Includes credit limit and hold status. |
customers.cancelled | customers:read | A customer was cancelled. |
orders.created | orders:read | An order, quote or service order was created. |
orders.updated | orders:read | An order header or any of its lines changed. Fires once for the order, not per line. |
orders.cancelled | orders:read | An order was cancelled. Committed stock has been released. |
orders.invoiced | invoices:read | An order produced one or more invoices. |
shipments.created | shipments:read | A shipment went out. The 856 trigger. |
shipments.cancelled | shipments:read | A shipment was cancelled and its stock movement reversed. |
invoices.posted | invoices:read | An invoice posted to the ledger. The 810 trigger. |
invoices.cancelled | invoices:read | An invoice was cancelled. |
payments.created | payments:read | A payment was applied to an invoice. |
payments.cancelled | payments:read | A payment was reversed. |
orderdeposits.created | payments:read | A deposit was taken against an order. |
purchaseorders.created | purchaseorders:read | A purchase order was raised. |
purchaseorders.updated | purchaseorders:read | A purchase order or one of its lines changed. |
purchaseorders.cancelled | purchaseorders:read | A purchase order was cancelled. |
invreceives.created | purchaseorders:read | Goods were received against a purchase order. |
items.created | items:read | An item was created. |
items.updated | items:read | An item changed. Price and description changes land here. |
items.stockchanged | items:read | On-hand or available quantity moved at a location. High volume - subscribe only if you mirror stock. |
serviceordertimes.billed | orders:read | Logged hours were converted to order lines and billed. |
itemidentifierdetails.created | items:read | A serial number, lot or expiry value was recorded against an item. |
itemidentifierdetails.updated | items:read | An identifier value changed, including its on-hand quantity at a location. |