Webhook Management

Manage your webhook endpoints to receive real-time event notifications. You can have up to 10 active webhooks per organization.

All webhook mutations (POST, PUT, DELETE, and the test endpoint) require an HMAC signature (see Authentication). Listing webhooks is API key only. Request bodies are strictly validated: unknown fields return 400.

Create Webhook

text
POST /webhooks

Request Body

json
{
	"url": "https://yourapp.com/webhooks/vignetim",
	"events": ["order.completed", "order.failed"],
	"description": "Production order notifications"
}
FieldTypeRequiredDescription
urlstringYesHTTPS endpoint URL to receive webhook deliveries
eventsstring[]Yes1-10 event types from: order.completed, order.fulfilled, order.failed, order.refunded, order.cancelled, *
descriptionstringNoA human-readable description, max 255 chars

Example Request

POST/v2/partners/webhooks
API_KEY="your-api-key"
API_SECRET="your-api-secret"

# Signing key = SHA-256 hex digest of your API secret
# $NF, not $2: OpenSSL prints "SHA2-256(stdin)= <hash>" but LibreSSL (the macOS
# default) prints the bare hash, where $2 is empty — that yields an empty signing
# key and a 401 with nothing to indicate why.
SIGNING_KEY=$(echo -n "${API_SECRET}" | openssl dgst -sha256 | awk '{print $NF}')

TIMESTAMP=$(date +%s)
NONCE=$(uuidgen | tr '[:upper:]' '[:lower:]')
METHOD="POST"
REQ_PATH="/v2/partners/webhooks"

# No request body: sign an empty body string.
BODY=""

MESSAGE="${TIMESTAMP}.${NONCE}.${METHOD}.${REQ_PATH}.${BODY}"

SIGNATURE=$(echo -n "${MESSAGE}" | \
  openssl dgst -sha256 -hmac "${SIGNING_KEY}" | \
  awk '{print $NF}')

curl -X POST "https://api.vignetim.com${REQ_PATH}" \
  -H "X-API-Key: ${API_KEY}" \
  -H "X-Timestamp: ${TIMESTAMP}" \
  -H "X-Nonce: ${NONCE}" \
  -H "X-Signature: ${SIGNATURE}"

Response (201 Created)

json
{
	"id": "1f2e3d4c-5b6a-7980-cdef-0123456789ab",
	"url": "https://yourapp.com/webhooks/vignetim",
	"events": ["order.completed", "order.failed"],
	"active": true,
	"description": "Production order notifications",
	"failureCount": 0,
	"createdAt": "2026-03-20T14:30:00.000Z",
	"signingSecret": "<64-char-hex-signing-secret>"
}

Important: The signingSecret (a 64-char hex string) is only returned when the webhook is first created. Store it securely; you need it to verify incoming webhook signatures and it cannot be retrieved again.

Creating an 11th active webhook returns 400.

SSRF Protection

Webhook URLs must be publicly reachable HTTPS endpoints. Rejected with 400:

  • Non-HTTPS URLs
  • Localhost, loopback, and internal hostnames (.local, .internal, cluster suffixes)
  • Hostnames resolving to private or link-local IP ranges (10.x, 172.16-31.x, 192.168.x, 169.254.x, CGNAT)
  • Cloud metadata endpoints

List Webhooks

text
GET /webhooks

API key only, no signature required.

Example Request

GET/v2/partners/webhooks
curl "https://api.vignetim.com/v2/partners/webhooks" \
  -H "X-API-Key: $VIGNETIM_API_KEY"

Response

json
{
	"data": [
		{
			"id": "1f2e3d4c-5b6a-7980-cdef-0123456789ab",
			"url": "https://yourapp.com/webhooks/vignetim",
			"events": ["order.completed", "order.failed"],
			"active": true,
			"description": "Production order notifications",
			"lastDeliveredAt": "2026-03-20T14:31:15.000Z",
			"failureCount": 0,
			"createdAt": "2026-03-20T14:30:00.000Z"
		}
	],
	"metadata": {
		"total": 1
	}
}
FieldTypeDescription
idUUIDWebhook ID
urlstringDelivery URL
eventsstring[]Subscribed events
activebooleanWhether the webhook receives deliveries
descriptionstringDescription (optional)
lastDeliveredAtISO 8601Timestamp of the last successful delivery (optional)
failureCountintegerConsecutive failed deliveries; resets to 0 on success
createdAtISO 8601Creation timestamp

Update Webhook

text
PUT /webhooks/:id

Path Parameters

NameTypeRequiredDescription
idUUIDYesThe webhook ID

Request Body

All fields are optional; only provided fields are updated.

json
{
	"url": "https://yourapp.com/webhooks/vignetim-v2",
	"events": ["*"],
	"active": true,
	"description": "Updated to receive all events"
}
FieldTypeDescription
urlstringNew HTTPS URL (re-validated against SSRF rules)
eventsstring[]New event subscription (max 10, same allowed values)
activebooleanSet true to re-enable an auto-disabled webhook, false to pause
descriptionstringNew description, max 255 chars

Example Request

PUT/v2/partners/webhooks/1f2e3d4c-5b6a-7980-cdef-0123456789ab
API_KEY="your-api-key"
API_SECRET="your-api-secret"

# Signing key = SHA-256 hex digest of your API secret
# $NF, not $2: OpenSSL prints "SHA2-256(stdin)= <hash>" but LibreSSL (the macOS
# default) prints the bare hash, where $2 is empty — that yields an empty signing
# key and a 401 with nothing to indicate why.
SIGNING_KEY=$(echo -n "${API_SECRET}" | openssl dgst -sha256 | awk '{print $NF}')

TIMESTAMP=$(date +%s)
NONCE=$(uuidgen | tr '[:upper:]' '[:lower:]')
METHOD="PUT"
REQ_PATH="/v2/partners/webhooks/1f2e3d4c-5b6a-7980-cdef-0123456789ab"

# No request body: sign an empty body string.
BODY=""

MESSAGE="${TIMESTAMP}.${NONCE}.${METHOD}.${REQ_PATH}.${BODY}"

SIGNATURE=$(echo -n "${MESSAGE}" | \
  openssl dgst -sha256 -hmac "${SIGNING_KEY}" | \
  awk '{print $NF}')

curl -X PUT "https://api.vignetim.com${REQ_PATH}" \
  -H "X-API-Key: ${API_KEY}" \
  -H "X-Timestamp: ${TIMESTAMP}" \
  -H "X-Nonce: ${NONCE}" \
  -H "X-Signature: ${SIGNATURE}"

Response

Returns the updated webhook object (same shape as list items, without signingSecret).

Delete Webhook

text
DELETE /webhooks/:id

Example Request

DELETE/v2/partners/webhooks/1f2e3d4c-5b6a-7980-cdef-0123456789ab
API_KEY="your-api-key"
API_SECRET="your-api-secret"

# Signing key = SHA-256 hex digest of your API secret
# $NF, not $2: OpenSSL prints "SHA2-256(stdin)= <hash>" but LibreSSL (the macOS
# default) prints the bare hash, where $2 is empty — that yields an empty signing
# key and a 401 with nothing to indicate why.
SIGNING_KEY=$(echo -n "${API_SECRET}" | openssl dgst -sha256 | awk '{print $NF}')

TIMESTAMP=$(date +%s)
NONCE=$(uuidgen | tr '[:upper:]' '[:lower:]')
METHOD="DELETE"
REQ_PATH="/v2/partners/webhooks/1f2e3d4c-5b6a-7980-cdef-0123456789ab"

# No request body: sign an empty body string.
BODY=""

MESSAGE="${TIMESTAMP}.${NONCE}.${METHOD}.${REQ_PATH}.${BODY}"

SIGNATURE=$(echo -n "${MESSAGE}" | \
  openssl dgst -sha256 -hmac "${SIGNING_KEY}" | \
  awk '{print $NF}')

curl -X DELETE "https://api.vignetim.com${REQ_PATH}" \
  -H "X-API-Key: ${API_KEY}" \
  -H "X-Timestamp: ${TIMESTAMP}" \
  -H "X-Nonce: ${NONCE}" \
  -H "X-Signature: ${SIGNATURE}"

Response

json
{
	"success": true
}

Test Webhook

Send a test event to verify that your endpoint is reachable and processes deliveries correctly. The payload is documented in Webhooks Overview.

text
POST /webhooks/:id/test

Example Request

The test endpoint takes no request body; sign an empty body string.

POST/v2/partners/webhooks/1f2e3d4c-5b6a-7980-cdef-0123456789ab/test
API_KEY="your-api-key"
API_SECRET="your-api-secret"

# Signing key = SHA-256 hex digest of your API secret
# $NF, not $2: OpenSSL prints "SHA2-256(stdin)= <hash>" but LibreSSL (the macOS
# default) prints the bare hash, where $2 is empty — that yields an empty signing
# key and a 401 with nothing to indicate why.
SIGNING_KEY=$(echo -n "${API_SECRET}" | openssl dgst -sha256 | awk '{print $NF}')

TIMESTAMP=$(date +%s)
NONCE=$(uuidgen | tr '[:upper:]' '[:lower:]')
METHOD="POST"
REQ_PATH="/v2/partners/webhooks/1f2e3d4c-5b6a-7980-cdef-0123456789ab/test"

# No request body: sign an empty body string.
BODY=""

MESSAGE="${TIMESTAMP}.${NONCE}.${METHOD}.${REQ_PATH}.${BODY}"

SIGNATURE=$(echo -n "${MESSAGE}" | \
  openssl dgst -sha256 -hmac "${SIGNING_KEY}" | \
  awk '{print $NF}')

curl -X POST "https://api.vignetim.com${REQ_PATH}" \
  -H "X-API-Key: ${API_KEY}" \
  -H "X-Timestamp: ${TIMESTAMP}" \
  -H "X-Nonce: ${NONCE}" \
  -H "X-Signature: ${SIGNATURE}"

Response

On a successful delivery:

json
{
	"success": true,
	"statusCode": 200
}

If the delivery fails:

json
{
	"success": false,
	"statusCode": 500
}

If the request could not be completed at all (timeout, DNS failure), statusCode is replaced by an error message:

json
{
	"success": false,
	"error": "The operation was aborted due to timeout"
}