Skip to main content
    Skip to content
    NorruvaDeveloper Docs
    Sandbox · verified 2026-07-29
    This page
    Whole docs
    Machine formats

    PlaygroundGet API keys
    IntroductionQuickstartAuthenticationErrors & conventionsSandbox & environments
    Entity modelProducts & categoriesPassports & versionsDigital Link & resolutionCompliance & regulationsWebhooks & eventsAutoID print loopImport jobsObservability & audit
    OverviewAuth & API keysProductsCompliance & regulationsPassportsResolver & publicWebhooksPrint jobs & devicesImport / bulkObservabilityBeyond happy path
    Integration playbookWebhook receiver guideRun a print deviceDeviations & gotchas
    EN 18222 API methodsAnnex ZA — ESPR correspondence
    Docs/API reference/Webhooks

    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
    KEYwebhooks:manage

    Register an endpoint + subscribed event set. Subscribe only to catalogue events — an unknown name returns 422 UNKNOWN_EVENT_TYPE. Live webhooks require https.

    Parameters
    urlrequiredbody · stringReceiver endpoint (https required in live).
    eventsrequiredbody · string[]Catalogue event names only.
    Request
    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}}
    }
    Errors you can branch on
    422 UNKNOWN_EVENT_TYPE403 API_SCOPE_DENIED
    Related
    Webhooks & eventsWebhook receiver guide
    KEYwebhooks:manage

    List webhook endpoints (paginated).

    Request
    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}}
    }
    Related
    Webhooks & events
    KEYwebhooks:manage

    Update the subscribed event set or endpoint.

    Request
    PUT {{baseUrl}}/api/v2/webhooks/{id}
    Authorization: Bearer {{apiKey}}
    Content-Type: application/json
    curl -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}}
    }
    Related
    Webhooks & events
    KEYwebhooks:manage

    Remove a webhook endpoint.

    Request
    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}}
    }
    Related
    Webhooks & events
    KEY

    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.

    Request
    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}}
    }
    Errors you can branch on
    404
    Related
    Deviations & gotchas
    KEYwebhooks:manage

    Test ping that records response code and latency.

    Request
    POST {{baseUrl}}/api/v2/webhooks/{id}/test
    Authorization: Bearer {{apiKey}}
    Content-Type: application/json
    curl -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}}
    }
    Related
    Webhook receiver guide
    KEYwebhooks:manage

    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.

    Request
    POST {{baseUrl}}/api/v2/webhooks/{id}/rotate-secret
    Authorization: Bearer {{apiKey}}
    Content-Type: application/json
    curl -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}}
    }
    Related
    Webhook receiver guide
    KEY

    Delivery attempts + status. The delivery id is stable across retries — use it for receiver-side dedup.

    Request
    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}}
    }
    Related
    Webhook receiver guide
    KEY

    Manual redelivery of a failed delivery.

    Request
    POST {{baseUrl}}/api/v2/webhooks/{id}/deliveries/{deliveryId}/retry
    Authorization: Bearer {{apiKey}}
    Content-Type: application/json
    curl -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}}
    }
    Related
    Webhooks & events

    Rotate secrets with rotate-secret — the 24 h overlap window means receivers can roll over without dropping in-flight deliveries.

    Was this page helpful?
    Thanks — noted.Feedback goes to the docs team by email.
    ← PreviousResolver & publicNext →Print jobs & devices
    On this page
    POST /webhooksGET /webhooksPUT /webhooks/{id}DELETE /webhooks/{id}GET /webhooks/{id}/eventsPOST /webhooks/{id}/testPOST /webhooks/{id}/rotate-secretGET /webhooks/{id}/deliveriesPOST /webhooks/{id}/deliveries/{deliveryId}/retry
    Norruva DPP API · sandbox developer docsGenerated 2026-07-29 · PRD-aligned (TSC roadmap rev 2) · statuses reflect E2E-verified sandbox behaviour — not marketing