Using Newforma Konekt's Webhooks
This page will help you get started with Newforma Konekt's Webhooks.
Use webhooks to receive event notifications from Newforma Konekt in near real time. Webhooks can be registered at the hub scope or at the project scope and are delivered with at-least-once semantics.
Concepts
| Term | Meaning |
|---|---|
| Hub-scoped webhook | Receives matching events across an entire hub. |
| Project-scoped webhook | Receives matching events only for one project. |
| Topic | Stable customer-facing token used for subscription matching and sent in X-Newforma-Event. |
Event body type | Source producer event identifier. For compatibility, this may differ from the topic token. Route on X-Newforma-Event if you need the stable public topic. |
eventId | Stable identifier for the event across retries. |
deliveryId | Per-attempt identifier. Use this value when contacting support about a failed delivery attempt. |
Delivery Model
- Deliveries are at-least-once.
- Retries use exponential backoff and can span roughly 24 hours.
- A webhook can be auto-paused after repeated failures.
- The per-hub limit is 100 non-archived webhooks.
Listener Requirements
- Use
https://on port443. - Return a
2xxresponse within15seconds. - Do not redirect requests.
- Expect request bodies up to roughly
256 KB. - Keep your response body at
8 KBor less. - Your endpoint must resolve to a public IP. Private, loopback, link-local, metadata, and similar addresses are blocked.
Authentication And Signing
Every delivery can include:
Authorization: Bearer <authToken>if the webhook was configured with an auth token.X-Newforma-Timestampwith the Unix timestamp used for signing.X-Newforma-Signaturewithsha256=<hex>where the signed payload is${timestamp}.${rawBody}.
Reject any request outside a 5 minute replay window and compare signatures in constant time.
Request Headers
| Header | Meaning |
|---|---|
Content-Type | Always application/json |
Authorization | Optional bearer token configured on the webhook |
X-Newforma-Event | Stable public topic token such as rfi.created |
X-Newforma-Event-Id | Stable event identifier |
X-Newforma-Delivery-Id | Per-attempt delivery identifier |
X-Newforma-Delivery-Attempt | Attempt number starting at 1 |
X-Newforma-Timestamp | Unix timestamp used for signing |
X-Newforma-Signature | sha256=<hex> signature over ${timestamp}.${rawBody} |
User-Agent | NewformaKonekt-Webhooks/1.0 |
Example Delivery
Headers:
X-Newforma-Event: rfi.created
X-Newforma-Event-Id: 9f13d80f-8e49-4d05-8f69-faf8c53eb52f
X-Newforma-Delivery-Id: 8f06a1f4-7d03-4580-949d-c2499d4e06a0
X-Newforma-Delivery-Attempt: 1
X-Newforma-Timestamp: 1780920000
X-Newforma-Signature: sha256=<hex>Body:
{
"eventId": "9f13d80f-8e49-4d05-8f69-faf8c53eb52f",
"type": "RFI_CREATED",
"hubId": "hub-001",
"projectId": "42",
"resourceId": "5001",
"occurredAt": "2026-06-08T18:20:00.000Z"
}Topics
Fetch the exact catalog for a hub with GET /hubs/{hubId}/webhooks/events. The current public categories are:
RFI topics
rfi.created, rfi.updated, rfi.deleted, rfi.logged, rfi.received, rfi.forwarded, rfi.reviewSubmitted, rfi.officialResponse, rfi.sent, rfi.closed, rfi.reopened
Submittal topics
submittal.created, submittal.updated, submittal.deleted, submittal.logged, submittal.received, submittal.forwarded, submittal.reviewSubmitted, submittal.officialResponse, submittal.sent, submittal.closed, submittal.reopened
Wildcard tokens:
*subscribes to every available topic in scope.rfi.*subscribes to all RFI topics.submittal.*subscribes to all submittal topics.
Management API
Every management route is authenticated with an OAuth 2.0 bearer token and authorized by your role in the target hub or project. The sections below explain how to obtain a token, which scopes it must carry, and who is allowed to manage webhooks.
Authentication methods
Newforma Konekt accepts three kinds of caller credentials. All three resolve to a Konekt user identity; they differ in how the token is obtained and how long it lives. Whatever method you choose, the token must carry the scopes listed in Required scopes below.
Hub owner master token
A hub owner can generate a long-lived master token for the hub. It authenticates as the hub owner — who is implicitly a hub administrator — so it satisfies the authorization checks for every hub-scoped and project-scoped route in that hub. Use it for unattended, server-to-server integrations where no interactive sign-in is possible. Because it is long-lived and grants hub-owner access, store it as a secret and rotate it if it leaks.
User token
A short-lived access token issued for an individual Konekt user. Actions are performed as that user and authorized by that user's hub or project role, so the user must be a hub or project administrator (see Authorization below). Access tokens expire after roughly two hours. User tokens are convenient for manual testing but are not suitable on their own for unattended integrations.
Client ID and secret (OAuth 2.0)
The recommended way to build a production integration. Register your application with the Newforma Konekt team to receive a client ID and secret and to whitelist your redirect URI, then run the PKCE-enhanced authorization code flow to obtain a user access token (and a refresh token when you request offline_access). The token acts as the user who signed in, so that user must be a hub or project administrator.
The full step-by-step flow — code verifier and challenge, /connect/authorize, and /connect/token — is documented in Using OAuth 2.0. Key production endpoints:
| Purpose | Endpoint |
|---|---|
| Authorization server | https://auth.bimtrackapp.co |
| Authorize | https://auth.bimtrackapp.co/connect/authorize |
| Token | https://auth.bimtrackapp.co/connect/token |
Access tokens live for roughly two hours; use the refresh token to renew without re-prompting the user.
Required scopes
Request the following scopes (space-delimited in the scope parameter) when you acquire the token. A token that is missing any of them is rejected even when the user holds the right role.
| Scope | Why it is required |
|---|---|
PIMTrack_Api | The webhook management endpoints are served by the PIM API. Without this scope every management call is rejected at the gateway. |
BIMTrack_Api | Authorization resolves your hub and project role through the Konekt core API. The service forwards your token to that API, so the token must be accepted there. |
SubscriptionService_Api_Read | Create, reactivate, and topic-change calls verify the hub's subscription entitlement through the Subscription Service. Without it those calls fail (see below). |
Also include the standard OpenID Connect scopes that the OAuth flow requires: openid (always required) and offline_access (only when you need a refresh token). A minimal scope value for full webhook management is:
openid PIMTrack_Api BIMTrack_Api SubscriptionService_Api_ReadSubscriptionService_Api_Read is the read-only subscription scope; the broader SubscriptionService_Api also satisfies the check. These scopes are enabled per registered client, so when you register an OAuth application ask the Newforma Konekt team to enable PIMTrack_Api and SubscriptionService_Api_Read for it.
Authorization
Regardless of how the token was obtained, the resolved user must hold the right role:
- Hub-scoped routes (
/hubs/{hubId}/webhooks...): a hub administrator or the hub owner. - Project-scoped routes (
/hubs/{hubId}/projects/{projectId}/webhooks...): a hub administrator, the hub owner, or an administrator of that project.
A token that authenticates but lacks the required role receives 403. A token that is missing SubscriptionService_Api_Read receives 403 with subscription_insufficient on the subscription-gated operations (create, reactivate, and topic changes), even for a hub administrator.
| Route | Methods | Description |
|---|---|---|
/hubs/{hubId}/webhooks/events | GET | List available topics and wildcard tokens. |
/hubs/{hubId}/webhooks | GET, POST | List or create hub-scoped webhooks. |
/hubs/{hubId}/webhooks/{webhookId} | GET, PATCH, DELETE | Read, update, or delete one hub-scoped webhook. |
/hubs/{hubId}/webhooks/{webhookId}/ping | POST | Send a synchronous test delivery to a hub-scoped webhook. |
/hubs/{hubId}/projects/{projectId}/webhooks | GET, POST | List or create project-scoped webhooks. |
/hubs/{hubId}/projects/{projectId}/webhooks/{webhookId} | GET, PATCH, DELETE | Read, update, or delete one project-scoped webhook. |
/hubs/{hubId}/projects/{projectId}/webhooks/{webhookId}/ping | POST | Send a synchronous test delivery to a project-scoped webhook. |
Create request
{
"url": "https://example.com/webhooks/newforma",
"topics": ["rfi.*"],
"authToken": "optional-token",
"expiresAt": "2026-12-31T23:59:59.000Z"
}Create response
{
"webhook": {
"id": "507f1f77bcf86cd799439011",
"scope": "hub",
"url": "https://example.com/webhooks/newforma",
"topics": ["rfi.*"],
"status": "active",
"consecutiveFailedDeliveries": 0,
"createdBy": "[email protected]",
"createdAt": "2026-06-08T18:20:00.000Z",
"updatedAt": "2026-06-08T18:20:00.000Z",
"updatedBy": "[email protected]"
},
"signingSecret": "whsec_..."
}Update request
{
"status": "paused",
"rotateSigningSecret": true,
"expiresAt": null
}Ping response
{
"success": true,
"statusCode": 200,
"durationMs": 42
}HMAC Verification Recipes
Node.js
import { createHmac, timingSafeEqual } from "node:crypto";
function verify({ secret, timestamp, signature, rawBody }) {
const ageSeconds = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
if (ageSeconds > 300) {
return false;
}
const expected = `sha256=${createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex")}`;
return timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}.NET
using System.Security.Cryptography;
using System.Text;
static bool Verify(string secret, string timestamp, string signature, string rawBody)
{
var ageSeconds = Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - long.Parse(timestamp));
if (ageSeconds > 300)
{
return false;
}
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var payload = Encoding.UTF8.GetBytes($"{timestamp}.{rawBody}");
var expected = $"sha256={Convert.ToHexString(hmac.ComputeHash(payload)).ToLowerInvariant()}";
return CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(expected),
Encoding.UTF8.GetBytes(signature)
);
}Python
import hashlib
import hmac
import time
def verify(secret: str, timestamp: str, signature: str, raw_body: str) -> bool:
if abs(int(time.time()) - int(timestamp)) > 300:
return False
signed_payload = f"{timestamp}.{raw_body}".encode("utf-8")
expected = "sha256=" + hmac.new(secret.encode("utf-8"), signed_payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)For other languages, compute sha256= + the hex-encoded HMAC-SHA256 of {timestamp}.{rawBody} with your signing secret, compare it to X-Newforma-Signature using a constant-time comparison, and reject the request if timestamp is more than 300 seconds old.
Retry Behavior
- Attempt
1uses the delivery queue. - Attempts
2and3are in-band SQS retries. - Attempts
4through10are scheduled at approximately10m,30m,1h,2h,6h,12h, and24hwith jitter. - After the final failure, the webhook is auto-paused with
statusReason = delivery_failures_exhausted.
Troubleshooting
| Symptom | Likely cause |
|---|---|
success = false from .../ping with statusCode = null | DNS, TLS, timeout, or SSRF guard rejection prevented the request. |
| Delivery retries but never succeeds | Endpoint is returning non-2xx, timing out, or redirecting. |
403 when creating or reactivating | The hub subscription does not include the functionality required by one or more requested topic categories. |
| No webhook arrives for an Azure producer | The platform-azure-notifications requiredSubscriptions.ts entry, or the azureIngress/{producer}.ts mapping or azureIngress/bundles.ts registration, is missing. |
When opening a support ticket, include the deliveryId, the approximate UTC time, and the webhook id.
Updated 6 days ago
