Developer Docs
Sandbox · API v2PlaygroundGet API keys

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
Related
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
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
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
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
Related
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
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
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
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

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.