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
- 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.
- 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.
- Test: in Sandbox Event testing, send a subscribed event and check Delivery logs for a receiver
2xxresponse. Queued does not mean received.
| Configuration | Requirement | What to do |
|---|---|---|
| Callback URL | 🔴 Required | Enter a public HTTPS URL that responds to connection verification |
| Subscribed events | 🔴 Required | Select at least one event needed by your business |
| HMAC Secret, Verification Token | Platform-generated | No input or custom values needed; reveal and store on your receiver |
| Encrypt Key | Optional | Message encryption is off by default; configure decryption only when enabled |
| Synchronous callbacks | Optional | Not 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:
POST /webhooks/goodvision
Content-Type: application/json
X-GV-Webhook-Verification: url_verification
{"type":"url_verification","challenge":"opaque_challenge","token":"opaque_verification_token"}HTTP/1.1 200 OK
Content-Type: application/json
{"challenge":"opaque_challenge"}Endpoint and response requirements:
| Item | Requirement |
|---|---|
| Endpoint | Publicly reachable HTTPS URL |
| HTTP status | 2xx, no redirects |
Content-Type | application/json |
| Body | At 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:
- Select a subscribed lifecycle event.
- Choose Send test event.
- 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.
| Response | Check |
|---|---|
204 | Receipt confirmed |
401/403 | Signature, time window, Nonce and Token |
422 | Schema and decrypted fields |
5xx or network failure | Receiver 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.
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/1.1 204 No ContentThis 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
| Header | Purpose |
|---|---|
X-GV-Delivery-ID | Identifies the delivery record for diagnostics and retries |
X-GV-Event-ID | Stable business event ID used for deduplication |
X-GV-Event-Type | Versioned event name |
X-GV-Request-Timestamp | Unix timestamp for this delivery attempt |
X-GV-Request-Nonce | Random value for this delivery attempt |
X-GV-Test-Delivery | true only for event tests; after signature verification, still check the signed event prefix |
X-GV-Signature | HMAC-SHA256 in sha256=<hex> format |
The signing input is exactly:
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.
- Check the timestamp against a five-minute window in either direction.
- Register the Nonce atomically in shared storage, keep it for at least ten minutes and reject duplicates.
- Compare the signature in constant time.
Replicated receivers need shared Nonce storage. Process memory cannot prevent replay across replicas.
Process in the correct order
- Read the raw Body. Require valid delivery ID, event ID, event type, timestamp, nonce, and signature headers.
- Check the time window and atomically reserve the nonce; reject expired or repeated requests with
401, and unavailable nonce storage with503. - Compute HMAC-SHA256 with the signing Secret and compare in constant time; return
401on mismatch. - Parse plaintext or decrypt
encryptaccording to endpoint configuration. Both modes require constant-timeverification_tokenvalidation; return401on mismatch. - 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. Return422on mismatch. - Persist an Inbox row idempotently by
event_id, with the token-free event,delivery_id, andcorrelation_id. Return503if persistence fails. The worker usesresource_versionto protect against stale updates. - Return
2xximmediately after the Inbox transaction commits; process slow business work asynchronously.
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 204This 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
| Credential | Purpose |
|---|---|
| HMAC Secret | Verify the outer raw HTTP Body |
| Verification Token | Check verification_token in the decoded complete event |
| Encrypt Key | Decrypt 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:
{
"encrypt": "<Base64 of IV followed by ciphertext>"
}Decrypt in this order:
- Verify the outer raw Body signature.
- Decode Base64; use the first 16 bytes as IV and the rest as ciphertext.
- Decrypt with AES-256-CBC using
SHA256(UTF-8 Encrypt Key)and remove PKCS#7 padding. - Parse JSON and compare the Token in constant time.
- 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:
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.
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, notdelivery_id. - Retries keep
delivery_id,event_id, andcorrelation_idstable. 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 onlyN+1; persistN+2as a pending gap. - Reconcile through an authorized status API when the missing event does not arrive within the contract window.
| Request or condition | Example HTTP response | Background behavior |
|---|---|---|
| Valid new event committed to the Inbox | 204 | Process once asynchronously |
Valid retry with a fresh nonce and an already-received event_id | 204 | Do not repeat business work |
| Reused nonce, expired timestamp, invalid signature or Token | 401 | Do not persist or process |
| Unsupported Schema, header/body mismatch or integration-scope mismatch | 422 | Reject and check event format and application settings |
| Inbox or nonce storage temporarily unavailable | 503 | Do not acknowledge; allow the platform to retry |
| Version 4 arrives while the current version is 2 | 204 after Inbox commit | Save 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 result | Action |
|---|---|
Network failure, timeout, 408, 425, 429 or documented 5xx | Automatic retries, up to eight attempts by default for business events |
400, 401, 403, 404, 410, 422 | Check endpoint, certificate, keys and Schema first |
| Repeated failures | Retain failed records and possibly pause the subscription; diagnose and recover in Console |
| Failed synthetic test | One 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.