Skip to content

Receive webhooks ​

For first-time integration, use the recommended Node.js long-connection example. It needs no public receiver URL or webhook encryption setup. The steps below apply when you choose HTTPS Webhooks.

Webhooks provide notifications for subscription activation, plan changes, cancellation, and other events. GoodVision sends events to the Application's configured HTTPS endpoint. Validate and persist each event before acknowledging it, then process business work asynchronously.

Delivery is at least once, so duplicates and out-of-order events are possible.

HTTP 2xx acknowledges durable receipt: persist the event before returning it. A lost response or consumer crash can cause retries, so deduplicate business work by event_id. If automatic retries are exhausted, review the failed delivery in the Console and request recovery using the original event_id.

Integration steps ​

  1. Configure delivery: open the current application → Events and webhooks → Add event. Enter a public HTTPS callback URL, test the connection, then select events and save. The platform generates the signing key and token and enables delivery automatically.
  2. Configure your receiver: open Receiver configuration, explicitly reveal the HMAC Secret and Verification Token, and store both on your server. Implement connection verification, signature validation and reliable acknowledgement as described below.
  3. Test: in Sandbox Event testing, send a subscribed event and check Delivery logs for a receiver 2xx response. Queued does not mean received.
ConfigurationRequirementWhat to do
Callback URL🔴 RequiredEnter a public HTTPS URL that responds to connection verification
Subscribed events🔴 RequiredSelect at least one event needed by your business
HMAC Secret, Verification TokenPlatform-generatedNo input or custom values needed; reveal and store on your receiver
Encrypt KeyOptionalMessage encryption is off by default; configure decryption only when enabled
Synchronous callbacksOptionalNot required for event delivery; synchronous requests and responses use separate HTTPS configuration

Configure callback URLs and events separately for each application. Applications may share a receiving service, but each uses its own credentials. Isolate signature verification, messages and business processing by application. See the event catalog.

Each application supports one non-revoked Webhook endpoint. Configure asynchronous event_types[] and synchronous callback_types[] separately. Preserve the other set's complete configuration when editing either.

Manage commercial subscriptions, API access grants and Webhook Subscriptions separately. Cancelling a commercial subscription does not revoke API access or stop Webhooks.

Recover a configuration save ​

If saving events, an endpoint or security configuration has an unknown result, the configuration dialog retains its content, revision and operation identifier and disables editing and closing the dialog. Use Recover original request to confirm the outcome instead of creating another save or rotation.

If recovery requires sign-in or verification, complete it and return to the same Application, then explicitly recover the original request. Verification never submits automatically, and temporarily displayed credentials are cleared. If the page explicitly says a credential cannot be delivered again, read its current revision, review it and confirm a new credential action. Do not request old plaintext values.

If the page explicitly displays “Recheck webhook access”, temporary credentials and previous confirmations are cleared. Choose “Recheck webhook permissions” to read current Application access and configuration; recheck again if that read temporarily fails. Success exposes only currently permitted actions without restoring old plaintext values or confirmations. Enter and confirm any subsequent save or rotation again. Rechecking never submits configuration.

Verify the endpoint ​

When creating a webhook or changing its endpoint URL, first select Test connection in Developer Console. The platform sends a verification request to the candidate URL, and the receiver must echo the challenge within one second:

http
POST /webhooks/goodvision
Content-Type: application/json
X-GV-Webhook-Verification: url_verification

{"type":"url_verification","challenge":"opaque_challenge","token":"opaque_verification_token"}
http
HTTP/1.1 200 OK
Content-Type: application/json

{"challenge":"opaque_challenge"}

Endpoint and response requirements:

ItemRequirement
EndpointPublicly reachable HTTPS URL
HTTP status2xx, no redirects
Content-Typeapplication/json
BodyAt most 64 KiB; only challenge

After a successful test, Console saves the configuration using a proof valid for five minutes. It binds the current organization, application and complete Endpoint URL. Test again if it expires or the URL changes.

For historical Webhooks without verification status, retest under Edit endpoint. Once verified, event or callback changes require no new test if the URL stays unchanged.

The verification request token is separate from the Verification Token used for event delivery. The challenge, token, and proof are temporary security material: never record, reuse, persist, or include them in logs, analytics, or support tickets.

Test complete events in the Console ​

In the current Sandbox application's Events & Webhooks → Event testing:

  1. Select a subscribed lifecycle event.
  2. Choose Send test event.
  3. Check receipt in the delivery logs.

The platform sends a complete synthetic subscription summary using the current Endpoint, HMAC Secret and Verification Token. Encryption uses the current Encrypt Key. All 19 subscription lifecycle event types are supported.

data.product_code uses the product code bound to the current application. Without a product binding, it uses the synthetic value webhook_test_product; this does not imply a real product is enabled. Each test has independent event and synthetic resource IDs, rather than simulating consecutive changes to one subscription. Use the examples below and the lifecycle guide to verify sequences such as scheduled cancellation followed by effective cancellation.

You need the current application's Webhook management permission. The Application and its single Webhook must be active, signing credentials must be delivered, and the endpoint must pass connection verification. Production, unverified or inactive resources, and conflicting configurations explain the blocking reason instead of sending a test.

Test queued means the platform accepted the request. Open Delivery logs, choose Read deliveries again, and find the returned Delivery ID, Event ID and Correlation ID.

ResponseCheck
204Receipt confirmed
401/403Signature, time window, Nonce and Token
422Schema and decrypted fields
5xx or network failureReceiver availability and required services

Tests make one attempt. Send a new test after fixing the receiver. Test failures do not pause normal Webhooks.

Verify the signature first, then check whether the signed body's event_id starts with evt_webhook_test_. X-GV-Test-Delivery: true is a hint and cannot identify a test request by itself.

Test customers, subscriptions, tenants, plans and operations are synthetic. Record results and acknowledge in a separate test branch. Do not update real subscriptions or member permissions, or query real APIs with synthetic IDs.

The command selects only event_type and the current configuration version. It cannot override the endpoint, Secret, Token, or business Body. The delivered producer uses the application's issued source value, unlike the display placeholder in the static example below.

Complete example: subscription activated ​

After a customer completes the subscription flow in the platform customer workspace, the receiver gets oem.subscription.activated.v1. This complete unencrypted request includes all required subscription-summary fields, rather than only changed fields:

You can also download the complete HTTP example. Content-Length counts UTF-8 Body bytes; recompute both length and signature after changing the Body.

http
POST /webhooks/goodvision HTTP/1.1
Host: receiver.example.com
Content-Type: application/json
Content-Length: 1185
X-GV-Delivery-ID: delivery_demo_activation
X-GV-Event-ID: evt_demo_activated
X-GV-Event-Type: oem.subscription.activated.v1
X-GV-Request-Timestamp: 1791244800
X-GV-Request-Nonce: nonce_demo_activation_001
X-GV-Signature: sha256=675ba54e260ff141e61a1090a6b4277710fc9b80e4205b55f4d0dc86fee56d53

{
  "event_id": "evt_demo_activated",
  "event_type": "oem.subscription.activated.v1",
  "schema_version": "1",
  "occurred_at": "2026-10-06T00:00:00Z",
  "producer": "opaque",
  "tenant_id": "org_customer_demo",
  "resource_id": "sub_demo_cancel",
  "resource_version": 2,
  "correlation_id": "req_demo_activation",
  "data": {
    "id": "sub_demo_cancel",
    "organization_id": "org_customer_demo",
    "open_platform_application_id": "app_demo_saas",
    "environment": "sandbox",
    "product_tenant_id": "tenant_demo_cancel",
    "party_id": "party_customer_demo",
    "order_id": "order_demo_initial",
    "product_code": "relaydesk",
    "plan_code": "basic_monthly_demo",
    "plan_version": 1,
    "status": "active",
    "current_period_start": "2026-10-06T00:00:00Z",
    "current_period_end": "2026-11-06T00:00:00Z",
    "status_effective_at": "2026-10-06T00:00:00Z",
    "payment_reference": "payment_demo_initial",
    "operation_id": "op_demo_activation",
    "reason_code": "example_activation",
    "resource_version": 2,
    "created_at": "2026-10-05T23:55:00Z",
    "updated_at": "2026-10-06T00:00:00Z"
  },
  "verification_token": "demo-verification-token-only"
}

IDs, plans and reason codes are synthetic. producer: "opaque" is a placeholder. Use the current application’s event Schema for actual enums and reason_code meanings; sample values cannot determine production behavior.

The signature uses the public demo value demo-signing-secret-only; the Verification Token is demo-verification-token-only. Neither is a valid credential. The signature covers the UTF-8 Body bytes, including the final LF newline. The fixed timestamp supports offline recomputation; sending it today fails the timestamp window.

After validation and a committed Inbox transaction, return an empty Body:

http
HTTP/1.1 204 No Content

This acknowledges reliable receipt, not completed plan or permission updates. Response bodies do not carry business results. A duplicate business event also receives 204 after request validation.

The event indicates that the commercial subscription is active. Before enabling member features, confirm member access through oem.access_context.changed.v1 and authorized queries. See member access handling.

Request headers and signature ​

HeaderPurpose
X-GV-Delivery-IDIdentifies the delivery record for diagnostics and retries
X-GV-Event-IDStable business event ID used for deduplication
X-GV-Event-TypeVersioned event name
X-GV-Request-TimestampUnix timestamp for this delivery attempt
X-GV-Request-NonceRandom value for this delivery attempt
X-GV-Test-Deliverytrue only for event tests; after signature verification, still check the signed event prefix
X-GV-SignatureHMAC-SHA256 in sha256=<hex> format

The signing input is exactly:

text
signing_input = timestamp + "\n" + nonce + "\n" + raw_body
expected = "sha256=" + hex(HMAC-SHA256(signing_secret, signing_input))

raw_body is the original HTTP byte sequence. Do not parse, format or reserialize JSON before signing.

  1. Check the timestamp against a five-minute window in either direction.
  2. Register the Nonce atomically in shared storage, keep it for at least ten minutes and reject duplicates.
  3. Compare the signature in constant time.

Replicated receivers need shared Nonce storage. Process memory cannot prevent replay across replicas.

Process in the correct order ​

  1. Read the raw Body. Require valid delivery ID, event ID, event type, timestamp, nonce, and signature headers.
  2. Check the time window and atomically reserve the nonce; reject expired or repeated requests with 401, and unavailable nonce storage with 503.
  3. Compute HMAC-SHA256 with the signing Secret and compare in constant time; return 401 on mismatch.
  4. Parse plaintext or decrypt encrypt according to endpoint configuration. Both modes require constant-time verification_token validation; return 401 on mismatch.
  5. Remove the transport field verification_token, then validate the issued event schema. Match event ID/type against headers; for subscription events also validate customer Organization, Application, environment, product, resource ID, and version. Return 422 on mismatch.
  6. Persist an Inbox row idempotently by event_id, with the token-free event, delivery_id, and correlation_id. Return 503 if persistence fails. The worker uses resource_version to protect against stale updates.
  7. Return 2xx immediately after the Inbox transaction commits; process slow business work asynchronously.
text
raw_body = request.raw_body
delivery_id = request.headers["X-GV-Delivery-ID"]
event_id = request.headers["X-GV-Event-ID"]
event_type = request.headers["X-GV-Event-Type"]
timestamp = request.headers["X-GV-Request-Timestamp"]
nonce = request.headers["X-GV-Request-Nonce"]
reject 401 unless timestamp_within_window(timestamp, 5_minutes)
reject 503 if nonce_store_unavailable
reject 401 unless reserve_nonce_once(nonce, 10_minutes)
expected = "sha256=" + hex(hmac_sha256(signing_secret, timestamp + "\n" + nonce + "\n" + raw_body))
reject 401 unless constant_time_equal(expected, request.headers["X-GV-Signature"])

transport = parse_json(raw_body)
if endpoint.encryption_enabled
  reject 422 unless transport has only the encrypt field
  transport = parse_json(decrypt_aes256cbc(transport.encrypt, encrypt_key))
end
reject 401 unless constant_time_equal(verification_token, transport.verification_token)
event = transport without verification_token
reject 422 unless validate_issued_event_schema(event)
reject 422 unless event.event_id == event_id
reject 422 unless event.event_type == event_type
reject 422 unless event.resource_id == event.data.id
reject 422 unless event.resource_version == event.data.resource_version
if event.event_id starts_with "evt_webhook_test_"
  reject 422 unless matches_configured_sandbox_application_product(event)
  reject 503 unless commit_test_inbox_if_new(event.event_id, event, delivery_id, event.correlation_id)
  return 204 // only record test results; never apply customer state or permissions
end
reject 422 unless matches_configured_customer_application_environment_product(event)
reject 503 unless commit_inbox_if_new(event.event_id, event, delivery_id, event.correlation_id)
return 204

This is subscription-event pseudocode. In a transaction, commit_inbox_if_new must distinguish a committed new event, an already-received identical event, and a storage failure using a unique key. The first two succeed. Endpoint verification uses the separate challenge branch above and is never stored as a subscription event.

Plaintext and encrypted bodies ​

CredentialPurpose
HMAC SecretVerify the outer raw HTTP Body
Verification TokenCheck verification_token in the decoded complete event
Encrypt KeyDecrypt events only when encryption is enabled

The three credentials serve signature verification, Token validation and decryption separately.

A plaintext body contains the complete event envelope plus top-level verification_token. Validate and remove the Token before checking the schema; otherwise a closed schema rejects it as an unknown field. Do not store the Token in Inbox, ordinary logs or support tickets.

Encrypted delivery encrypts the same complete JSON including the Verification Token. Its outer Body contains only this field. This is a format illustration; replace the angle-bracket placeholder with actual Base64:

json
{
  "encrypt": "<Base64 of IV followed by ciphertext>"
}

Decrypt in this order:

  1. Verify the outer raw Body signature.
  2. Decode Base64; use the first 16 bytes as IV and the rest as ciphertext.
  3. Decrypt with AES-256-CBC using SHA256(UTF-8 Encrypt Key) and remove PKCS#7 padding.
  4. Parse JSON and compare the Token in constant time.
  5. Remove the Token and validate the event Schema.

Use the Endpoint's configured mode. Reject decryption failures instead of falling back to plaintext.

Recompute a signature locally ​

Download the activation JSON and offline script into the same directory. Run with Node.js 18 or later:

bash
node "webhook-signature.mjs"

The script uses public demo credentials, the current time and a random Nonce to print a synthetic request with all headers and body. It makes no network request. Use it to verify raw-byte signing.

Before actual Schema validation, replace producer and integration settings with current application values. Production receivers also need time-window, Nonce, Token, Schema, scope and persistence checks.

js
import { createHmac, randomBytes } from "node:crypto";
import { readFileSync } from "node:fs";

// Offline example only: these public demo credentials are never valid credentials.
const signingSecret = "demo-signing-secret-only";
const verificationToken = "demo-verification-token-only";
const event = JSON.parse(readFileSync(new URL("./subscription-activated.json", import.meta.url), "utf8"));
const timestamp = String(Math.floor(Date.now() / 1000));
const nonce = randomBytes(16).toString("hex");
const rawBody = Buffer.from(JSON.stringify({ ...event, verification_token: verificationToken }), "utf8");
const signature = createHmac("sha256", signingSecret)
  .update(`${timestamp}\n${nonce}\n`, "utf8")
  .update(rawBody)
  .digest("hex");

// Print a synthetic request; do not send it or log a real event body.
process.stdout.write([
  "POST /webhooks/goodvision HTTP/1.1",
  "Host: receiver.example.com",
  "Content-Type: application/json",
  `Content-Length: ${rawBody.length}`,
  "X-GV-Delivery-ID: delivery_demo_activation",
  `X-GV-Event-ID: ${event.event_id}`,
  `X-GV-Event-Type: ${event.event_type}`,
  `X-GV-Request-Timestamp: ${timestamp}`,
  `X-GV-Request-Nonce: ${nonce}`,
  `X-GV-Signature: sha256=${signature}`,
  "",
  rawBody.toString("utf8"),
].join("\r\n"));

Duplicates and ordering ​

  • Use a uniqueness constraint on event_id, not delivery_id.
  • Retries keep delivery_id, event_id, and correlation_id stable. Every attempt generates a new timestamp, nonce, and encryption IV and is signed again.
  • Return success for duplicates without repeating an order, cancellation, payment, or refund operation.
  • At current resource version N, directly apply only N+1; persist N+2 as a pending gap.
  • Reconcile through an authorized status API when the missing event does not arrive within the contract window.
Request or conditionExample HTTP responseBackground behavior
Valid new event committed to the Inbox204Process once asynchronously
Valid retry with a fresh nonce and an already-received event_id204Do not repeat business work
Reused nonce, expired timestamp, invalid signature or Token401Do not persist or process
Unsupported Schema, header/body mismatch or integration-scope mismatch422Reject and check event format and application settings
Inbox or nonce storage temporarily unavailable503Do not acknowledge; allow the platform to retry
Version 4 arrives while the current version is 2204 after Inbox commitSave the version gap; wait for version 3 or query an authorized state API

Use delivery_id for delivery records, event_id for business deduplication, and correlation_id for diagnostics. Store full token-free events only in a controlled Inbox, never browser logs, ordinary application logs, or support tickets.

Failure and recovery ​

Delivery resultAction
Network failure, timeout, 408, 425, 429 or documented 5xxAutomatic retries, up to eight attempts by default for business events
400, 401, 403, 404, 410, 422Check endpoint, certificate, keys and Schema first
Repeated failuresRetain failed records and possibly pause the subscription; diagnose and recover in Console
Failed synthetic testOne attempt only; fix and send a new test

After fixing the endpoint or signature settings, query current state, then ask support to redeliver the original event_id. Redelivery uses the original record and content, with idempotency and audit records, and creates no new business event. The platform performs this operation; third parties do not call administration APIs.

Synchronous callback availability ​

See Handle synchronous callbacks for separate request and response examples and unknown-result handling.

Developer Console can configure token.agent.action.v1 on the same Endpoint and send one synthetic test from Sandbox Callback configuration, with a separate result. The real TOKEN business producer is not connected yet. Saving or passing a test does not prove production business invocation.

Validate signatures and decrypt according to the current configuration and issued request/response schema, then return a structured result within five seconds. Synchronous callbacks use separate processing rules without asynchronous event retries or failed-delivery recovery. Unknown execution is not retried automatically; verify the outcome before deliberately starting a new test.

Signing Secret and rotation ​

  • Use the Webhook signing Secret only for HMAC verification of its current Subscription. Never reuse an OAuth Client Secret, API Key, or another Application's credential.
  • A new signing Secret becomes active only after secure delivery succeeds. The old version remains active when delivery fails. The console shows only the version and delivery status, never the Secret value.
  • During the rotation window, the receiver may verify with the current and previous versions. Remove the previous version promptly after the switch completes.
  • Never put a plaintext Secret in a URL, log, analytics event, cache, support ticket, or ordinary database field.

Obtain the signing key securely from the console ​

For webhooks using console delivery, open Receiver configuration and choose Reveal HMAC Secret temporarily. The application owner or an authorized collaborator obtains the current key through their authenticated session and stores it on the receiving server. Leaving the page, changing applications or organizations, losing access, or changing the signing key version clears the temporary value. External delivery channels retain their existing delivery process. Delivery to the credential channel does not prove receiver configuration or event receipt; verify both through event testing and delivery logs.

Correlate membership events with signed-in users ​

Membership-related oem.access_context.changed.v1 uses data.subject_id to match space.membership_id from user context, not sub or a public alias. Correlate subscriptions by Application, environment, customer space and subscription ID, not the last signed-in person.

Developer documentation. Internal operations access is not required.