Webhooks
Manage subscriptions and inspect deliveries. The receiver-side contract (signatures, replay, dedup) is in the receiver guide.
| POST | /api/v2/webhooksCreate a webhook |
| GET | /api/v2/webhooksList webhooks |
| PUT | /api/v2/webhooks/{id}Update a webhook |
| DELETE | /api/v2/webhooks/{id}Delete a webhook |
| GET | /api/v2/webhooks/{id}/eventsWebhook events (deviation) |
| POST | /api/v2/webhooks/{id}/testSend a test ping |
| POST | /api/v2/webhooks/{id}/rotate-secretRotate the signing secret |
| GET | /api/v2/webhooks/{id}/deliveriesList deliveries |
| POST | /api/v2/webhooks/{id}/deliveries/{deliveryId}/retryRetry a delivery |
Register an endpoint + subscribed event set. Subscribe only to catalogue events — an unknown name returns 422 UNKNOWN_EVENT_TYPE. Live webhooks require https.
POST {{baseUrl}}/api/v2/webhooks
Authorization: Bearer {{apiKey}}
Content-Type: application/json
{
"url": "https://integrator.example/hooks/norruva",
"events": ["product.published", "compliance.verified", "import.completed"]
}curl -X POST "$BASE_URL/api/v2/webhooks" \
-H "Authorization: Bearer $NORRUVA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://integrator.example/hooks/norruva",
"events": [
"product.published",
"compliance.verified",
"import.completed"
]
}'const baseUrl = process.env.NORRUVA_BASE_URL;
const apiKey = process.env.NORRUVA_API_KEY;
const res = await fetch(`${baseUrl}/api/v2/webhooks`, {
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": `application/json`
},
body: JSON.stringify({
"url": "https://integrator.example/hooks/norruva",
"events": [
"product.published",
"compliance.verified",
"import.completed"
]
})
});
if (!res.ok) {
const err = await res.json(); // typed envelope: { error: { code, message, details? } }
throw new Error(`${res.status} ${err.error?.code}: ${err.error?.message}`);
}
const data = await res.json();import os, uuid, requests
base_url = os.environ["NORRUVA_BASE_URL"]
api_key = os.environ["NORRUVA_API_KEY"]
resp = requests.post(
f"{base_url}/api/v2/webhooks",
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": f"application/json"
},
json={
"url": "https://integrator.example/hooks/norruva",
"events": [
"product.published",
"compliance.verified",
"import.completed"
]
},
)
resp.raise_for_status() # error body is the typed envelope: {"error": {"code", "message", "details"}}
data = resp.json()package main
import (
"bytes"
"fmt"
"net/http"
"os"
)
func main() {
baseURL := os.Getenv("NORRUVA_BASE_URL")
apiKey := os.Getenv("NORRUVA_API_KEY")
url := fmt.Sprintf("%s/api/v2/webhooks", baseURL)
payload := []byte(`{
"url": "https://integrator.example/hooks/norruva",
"events": [
"product.published",
"compliance.verified",
"import.completed"
]
}`)
req, _ := http.NewRequest("POST", url, bytes.NewBuffer(payload))
req.Header.Set("Authorization", "Bearer "+apiKey+"")
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
fmt.Println(resp.Status) // non-2xx bodies use the typed envelope {error:{code,message,details}}
}List webhook endpoints (paginated).
GET {{baseUrl}}/api/v2/webhooks
Authorization: Bearer {{apiKey}}curl -X GET "$BASE_URL/api/v2/webhooks" \
-H "Authorization: Bearer $NORRUVA_API_KEY"const baseUrl = process.env.NORRUVA_BASE_URL;
const apiKey = process.env.NORRUVA_API_KEY;
const res = await fetch(`${baseUrl}/api/v2/webhooks`, {
method: "GET",
headers: {
"Authorization": `Bearer ${apiKey}`
}
});
if (!res.ok) {
const err = await res.json(); // typed envelope: { error: { code, message, details? } }
throw new Error(`${res.status} ${err.error?.code}: ${err.error?.message}`);
}
const data = await res.json();import os, uuid, requests
base_url = os.environ["NORRUVA_BASE_URL"]
api_key = os.environ["NORRUVA_API_KEY"]
resp = requests.get(
f"{base_url}/api/v2/webhooks",
headers={
"Authorization": f"Bearer {api_key}"
},
)
resp.raise_for_status() # error body is the typed envelope: {"error": {"code", "message", "details"}}
data = resp.json()package main
import (
"fmt"
"net/http"
"os"
)
func main() {
baseURL := os.Getenv("NORRUVA_BASE_URL")
apiKey := os.Getenv("NORRUVA_API_KEY")
url := fmt.Sprintf("%s/api/v2/webhooks", baseURL)
req, _ := http.NewRequest("GET", url, nil)
req.Header.Set("Authorization", "Bearer "+apiKey+"")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
fmt.Println(resp.Status) // non-2xx bodies use the typed envelope {error:{code,message,details}}
}Update the subscribed event set or endpoint.
PUT {{baseUrl}}/api/v2/webhooks/{id}
Authorization: Bearer {{apiKey}}
Content-Type: application/jsoncurl -X PUT "$BASE_URL/api/v2/webhooks/{id}" \
-H "Authorization: Bearer $NORRUVA_API_KEY" \
-H "Content-Type: application/json"const baseUrl = process.env.NORRUVA_BASE_URL;
const apiKey = process.env.NORRUVA_API_KEY;
const id = "…";
const res = await fetch(`${baseUrl}/api/v2/webhooks/${id}`, {
method: "PUT",
headers: {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": `application/json`
}
});
if (!res.ok) {
const err = await res.json(); // typed envelope: { error: { code, message, details? } }
throw new Error(`${res.status} ${err.error?.code}: ${err.error?.message}`);
}
const data = await res.json();import os, uuid, requests
base_url = os.environ["NORRUVA_BASE_URL"]
api_key = os.environ["NORRUVA_API_KEY"]
id = "…"
resp = requests.put(
f"{base_url}/api/v2/webhooks/{id}",
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": f"application/json"
},
)
resp.raise_for_status() # error body is the typed envelope: {"error": {"code", "message", "details"}}
data = resp.json()package main
import (
"fmt"
"net/http"
"os"
)
func main() {
baseURL := os.Getenv("NORRUVA_BASE_URL")
apiKey := os.Getenv("NORRUVA_API_KEY")
id := "…"
url := fmt.Sprintf("%s/api/v2/webhooks/%s", baseURL, id)
req, _ := http.NewRequest("PUT", url, nil)
req.Header.Set("Authorization", "Bearer "+apiKey+"")
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
fmt.Println(resp.Status) // non-2xx bodies use the typed envelope {error:{code,message,details}}
}Remove a webhook endpoint.
DELETE {{baseUrl}}/api/v2/webhooks/{id}
Authorization: Bearer {{apiKey}}curl -X DELETE "$BASE_URL/api/v2/webhooks/{id}" \
-H "Authorization: Bearer $NORRUVA_API_KEY"const baseUrl = process.env.NORRUVA_BASE_URL;
const apiKey = process.env.NORRUVA_API_KEY;
const id = "…";
const res = await fetch(`${baseUrl}/api/v2/webhooks/${id}`, {
method: "DELETE",
headers: {
"Authorization": `Bearer ${apiKey}`
}
});
if (!res.ok) {
const err = await res.json(); // typed envelope: { error: { code, message, details? } }
throw new Error(`${res.status} ${err.error?.code}: ${err.error?.message}`);
}
const data = await res.json();import os, uuid, requests
base_url = os.environ["NORRUVA_BASE_URL"]
api_key = os.environ["NORRUVA_API_KEY"]
id = "…"
resp = requests.delete(
f"{base_url}/api/v2/webhooks/{id}",
headers={
"Authorization": f"Bearer {api_key}"
},
)
resp.raise_for_status() # error body is the typed envelope: {"error": {"code", "message", "details"}}
data = resp.json()package main
import (
"fmt"
"net/http"
"os"
)
func main() {
baseURL := os.Getenv("NORRUVA_BASE_URL")
apiKey := os.Getenv("NORRUVA_API_KEY")
id := "…"
url := fmt.Sprintf("%s/api/v2/webhooks/%s", baseURL, id)
req, _ := http.NewRequest("DELETE", url, nil)
req.Header.Set("Authorization", "Bearer "+apiKey+"")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
fmt.Println(resp.Status) // non-2xx bodies use the typed envelope {error:{code,message,details}}
}Documented deviation: resolves ERP-integration ids (connector-events surface) — returns 404 for a webhook subscription id. A hook's subscribed events are on the webhook object itself.
GET {{baseUrl}}/api/v2/webhooks/{id}/events
Authorization: Bearer {{apiKey}}curl -X GET "$BASE_URL/api/v2/webhooks/{id}/events" \
-H "Authorization: Bearer $NORRUVA_API_KEY"const baseUrl = process.env.NORRUVA_BASE_URL;
const apiKey = process.env.NORRUVA_API_KEY;
const id = "…";
const res = await fetch(`${baseUrl}/api/v2/webhooks/${id}/events`, {
method: "GET",
headers: {
"Authorization": `Bearer ${apiKey}`
}
});
if (!res.ok) {
const err = await res.json(); // typed envelope: { error: { code, message, details? } }
throw new Error(`${res.status} ${err.error?.code}: ${err.error?.message}`);
}
const data = await res.json();import os, uuid, requests
base_url = os.environ["NORRUVA_BASE_URL"]
api_key = os.environ["NORRUVA_API_KEY"]
id = "…"
resp = requests.get(
f"{base_url}/api/v2/webhooks/{id}/events",
headers={
"Authorization": f"Bearer {api_key}"
},
)
resp.raise_for_status() # error body is the typed envelope: {"error": {"code", "message", "details"}}
data = resp.json()package main
import (
"fmt"
"net/http"
"os"
)
func main() {
baseURL := os.Getenv("NORRUVA_BASE_URL")
apiKey := os.Getenv("NORRUVA_API_KEY")
id := "…"
url := fmt.Sprintf("%s/api/v2/webhooks/%s/events", baseURL, id)
req, _ := http.NewRequest("GET", url, nil)
req.Header.Set("Authorization", "Bearer "+apiKey+"")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
fmt.Println(resp.Status) // non-2xx bodies use the typed envelope {error:{code,message,details}}
}Test ping that records response code and latency.
POST {{baseUrl}}/api/v2/webhooks/{id}/test
Authorization: Bearer {{apiKey}}
Content-Type: application/jsoncurl -X POST "$BASE_URL/api/v2/webhooks/{id}/test" \
-H "Authorization: Bearer $NORRUVA_API_KEY" \
-H "Content-Type: application/json"const baseUrl = process.env.NORRUVA_BASE_URL;
const apiKey = process.env.NORRUVA_API_KEY;
const id = "…";
const res = await fetch(`${baseUrl}/api/v2/webhooks/${id}/test`, {
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": `application/json`
}
});
if (!res.ok) {
const err = await res.json(); // typed envelope: { error: { code, message, details? } }
throw new Error(`${res.status} ${err.error?.code}: ${err.error?.message}`);
}
const data = await res.json();import os, uuid, requests
base_url = os.environ["NORRUVA_BASE_URL"]
api_key = os.environ["NORRUVA_API_KEY"]
id = "…"
resp = requests.post(
f"{base_url}/api/v2/webhooks/{id}/test",
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": f"application/json"
},
)
resp.raise_for_status() # error body is the typed envelope: {"error": {"code", "message", "details"}}
data = resp.json()package main
import (
"fmt"
"net/http"
"os"
)
func main() {
baseURL := os.Getenv("NORRUVA_BASE_URL")
apiKey := os.Getenv("NORRUVA_API_KEY")
id := "…"
url := fmt.Sprintf("%s/api/v2/webhooks/%s/test", baseURL, id)
req, _ := http.NewRequest("POST", url, nil)
req.Header.Set("Authorization", "Bearer "+apiKey+"")
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
fmt.Println(resp.Status) // non-2xx bodies use the typed envelope {error:{code,message,details}}
}New whsec_… shown once; the previous secret keeps verifying for a 24 h overlap window so receivers can roll over without dropping in-flight deliveries.
POST {{baseUrl}}/api/v2/webhooks/{id}/rotate-secret
Authorization: Bearer {{apiKey}}
Content-Type: application/jsoncurl -X POST "$BASE_URL/api/v2/webhooks/{id}/rotate-secret" \
-H "Authorization: Bearer $NORRUVA_API_KEY" \
-H "Content-Type: application/json"const baseUrl = process.env.NORRUVA_BASE_URL;
const apiKey = process.env.NORRUVA_API_KEY;
const id = "…";
const res = await fetch(`${baseUrl}/api/v2/webhooks/${id}/rotate-secret`, {
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": `application/json`
}
});
if (!res.ok) {
const err = await res.json(); // typed envelope: { error: { code, message, details? } }
throw new Error(`${res.status} ${err.error?.code}: ${err.error?.message}`);
}
const data = await res.json();import os, uuid, requests
base_url = os.environ["NORRUVA_BASE_URL"]
api_key = os.environ["NORRUVA_API_KEY"]
id = "…"
resp = requests.post(
f"{base_url}/api/v2/webhooks/{id}/rotate-secret",
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": f"application/json"
},
)
resp.raise_for_status() # error body is the typed envelope: {"error": {"code", "message", "details"}}
data = resp.json()package main
import (
"fmt"
"net/http"
"os"
)
func main() {
baseURL := os.Getenv("NORRUVA_BASE_URL")
apiKey := os.Getenv("NORRUVA_API_KEY")
id := "…"
url := fmt.Sprintf("%s/api/v2/webhooks/%s/rotate-secret", baseURL, id)
req, _ := http.NewRequest("POST", url, nil)
req.Header.Set("Authorization", "Bearer "+apiKey+"")
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
fmt.Println(resp.Status) // non-2xx bodies use the typed envelope {error:{code,message,details}}
}Delivery attempts + status. The delivery id is stable across retries — use it for receiver-side dedup.
GET {{baseUrl}}/api/v2/webhooks/{id}/deliveries
Authorization: Bearer {{apiKey}}curl -X GET "$BASE_URL/api/v2/webhooks/{id}/deliveries" \
-H "Authorization: Bearer $NORRUVA_API_KEY"const baseUrl = process.env.NORRUVA_BASE_URL;
const apiKey = process.env.NORRUVA_API_KEY;
const id = "…";
const res = await fetch(`${baseUrl}/api/v2/webhooks/${id}/deliveries`, {
method: "GET",
headers: {
"Authorization": `Bearer ${apiKey}`
}
});
if (!res.ok) {
const err = await res.json(); // typed envelope: { error: { code, message, details? } }
throw new Error(`${res.status} ${err.error?.code}: ${err.error?.message}`);
}
const data = await res.json();import os, uuid, requests
base_url = os.environ["NORRUVA_BASE_URL"]
api_key = os.environ["NORRUVA_API_KEY"]
id = "…"
resp = requests.get(
f"{base_url}/api/v2/webhooks/{id}/deliveries",
headers={
"Authorization": f"Bearer {api_key}"
},
)
resp.raise_for_status() # error body is the typed envelope: {"error": {"code", "message", "details"}}
data = resp.json()package main
import (
"fmt"
"net/http"
"os"
)
func main() {
baseURL := os.Getenv("NORRUVA_BASE_URL")
apiKey := os.Getenv("NORRUVA_API_KEY")
id := "…"
url := fmt.Sprintf("%s/api/v2/webhooks/%s/deliveries", baseURL, id)
req, _ := http.NewRequest("GET", url, nil)
req.Header.Set("Authorization", "Bearer "+apiKey+"")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
fmt.Println(resp.Status) // non-2xx bodies use the typed envelope {error:{code,message,details}}
}Manual redelivery of a failed delivery.
POST {{baseUrl}}/api/v2/webhooks/{id}/deliveries/{deliveryId}/retry
Authorization: Bearer {{apiKey}}
Content-Type: application/jsoncurl -X POST "$BASE_URL/api/v2/webhooks/{id}/deliveries/{deliveryId}/retry" \
-H "Authorization: Bearer $NORRUVA_API_KEY" \
-H "Content-Type: application/json"const baseUrl = process.env.NORRUVA_BASE_URL;
const apiKey = process.env.NORRUVA_API_KEY;
const id = "…";
const deliveryId = "…";
const res = await fetch(`${baseUrl}/api/v2/webhooks/${id}/deliveries/${deliveryId}/retry`, {
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": `application/json`
}
});
if (!res.ok) {
const err = await res.json(); // typed envelope: { error: { code, message, details? } }
throw new Error(`${res.status} ${err.error?.code}: ${err.error?.message}`);
}
const data = await res.json();import os, uuid, requests
base_url = os.environ["NORRUVA_BASE_URL"]
api_key = os.environ["NORRUVA_API_KEY"]
id = "…"
delivery_id = "…"
resp = requests.post(
f"{base_url}/api/v2/webhooks/{id}/deliveries/{delivery_id}/retry",
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": f"application/json"
},
)
resp.raise_for_status() # error body is the typed envelope: {"error": {"code", "message", "details"}}
data = resp.json()package main
import (
"fmt"
"net/http"
"os"
)
func main() {
baseURL := os.Getenv("NORRUVA_BASE_URL")
apiKey := os.Getenv("NORRUVA_API_KEY")
id := "…"
deliveryId := "…"
url := fmt.Sprintf("%s/api/v2/webhooks/%s/deliveries/%s/retry", baseURL, id, deliveryId)
req, _ := http.NewRequest("POST", url, nil)
req.Header.Set("Authorization", "Bearer "+apiKey+"")
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
fmt.Println(resp.Status) // non-2xx bodies use the typed envelope {error:{code,message,details}}
}Rotate secrets with rotate-secret — the 24 h overlap window means receivers can roll over without dropping in-flight deliveries.