Skip to main content

TurboWebhooks Java SDK

Let an agent scaffold this for you

Skills count

Install the TurboDocx Quickstart Skill and let Claude Code, Cursor, Copilot, Codex, or any agent that speaks the Agent Skills standard install the SDK, wire routes into your app, and write a working TurboWebhooks integration end-to-end.

$npx skills add TurboDocx/quickstart
Then run/turbodocx-sdk turbowebhooksin your agent.
View on GitHub

The official TurboDocx Webhooks SDK for Java applications (Spring Boot, Servlet, Jakarta EE, etc.). Subscribe a single per-organization HTTPS endpoint to TurboDocx signature events, verify inbound signatures with HMAC-SHA256, replay delivery attempts, and rotate secrets — all from Java 11+. Distributed as com.turbodocx:turbodocx-sdk on Maven Central (same artifact as TurboSign).


What is TurboWebhooks?

TurboWebhooks lets your application receive real-time notifications when signature documents complete or get voided, instead of polling the API. Each organization has a single, named webhook (signature) that mirrors the Signature Webhooks page in the dashboard, so SDK-managed and UI-managed configuration stays in sync.

For the full conceptual overview of how webhooks work in TurboSign (delivery retries, payload schema, dashboard UI), see TurboSign → Webhooks.

Installation

<dependency>
<groupId>com.turbodocx</groupId>
<artifactId>turbodocx-sdk</artifactId>
<version>0.2.0</version>
</dependency>

Then import:

import com.turbodocx.TurboDocxClient;
import com.turbodocx.TurboWebhooks;
import com.turbodocx.TurboDocxException;
import com.turbodocx.WebhookSignatureVerifier;

Requirements

  • Java 11 or higher
  • An administrator TurboDocx API key (the webhook routes are gated on the administrator role — non-admin keys return HTTP 403)
  • All TurboWebhooks methods return com.google.gson.JsonObject for forward compatibility — new server fields surface without an SDK upgrade

Configuration

import com.turbodocx.TurboDocxClient;
import com.turbodocx.TurboWebhooks;

TurboWebhooks webhooks = new TurboDocxClient.Builder()
.apiKey(System.getenv("TURBODOCX_API_KEY"))
.orgId(System.getenv("TURBODOCX_ORG_ID"))
.buildWebhooksClient();

buildWebhooksClient() does not require senderEmail — webhook routes don't send email, so the sender validation that build() enforces for TurboSign is skipped here. The returned TurboWebhooks is an admin-scoped client; construct once and reuse.

Environment Variables

TURBODOCX_API_KEY=your_admin_api_key
TURBODOCX_ORG_ID=your_org_id
# optional — defaults to https://api.turbodocx.com
TURBODOCX_BASE_URL=https://api.turbodocx.com
# store the secret returned by createWebhook so your receiver can verify signatures
TURBODOCX_WEBHOOK_SECRET=whsec_...
Administrator role required

TurboWebhooks endpoints require the administrator role on the API key. A valid TDX- key without the role throws TurboDocxException.AuthorizationException (HTTP 403). Generate or rotate keys in the Settings → API Keys page.

Quick Start

1. Create the signature webhook

import com.turbodocx.TurboDocxClient;
import com.turbodocx.TurboDocxException;
import com.turbodocx.TurboWebhooks;
import com.google.gson.JsonObject;

import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;

public class CreateSignatureWebhook {
public static void main(String[] args) throws Exception {
TurboWebhooks webhooks = new TurboDocxClient.Builder()
.apiKey(System.getenv("TURBODOCX_API_KEY"))
.orgId(System.getenv("TURBODOCX_ORG_ID"))
.buildWebhooksClient();

try {
JsonObject created = webhooks.createWebhook(
Arrays.asList("https://your-server.example.com/webhooks/turbodocx"),
Arrays.asList("signature.document.completed", "signature.document.voided")
);

// SAVE THIS SECRET — it is shown ONCE and cannot be retrieved later.
Files.writeString(Paths.get(".secret"), created.get("secret").getAsString());
System.out.println("Created webhook id=" + created.get("id").getAsString());

} catch (TurboDocxException.ConflictException e) {
// 409 — the signature webhook already exists for this org.
// Use updateWebhook or deleteWebhook instead.
System.out.println("Webhook already exists. Use updateWebhook or deleteWebhook.");
} catch (TurboDocxException.ValidationException e) {
// 400 — most commonly a non-HTTPS URL or empty events list.
System.err.println("Validation failed: " + e.getMessage());
}
}
}
HTTPS only

TurboDocx rejects non-HTTPS webhook URLs with HTTP 400. For local development, expose your receiver via an HTTPS tunnel (ngrok, cloudflared, or webhook.site) and pass the tunnel URL to createWebhook.

2. Verify inbound webhook signatures

When TurboDocx POSTs to your receiver, every request carries an X-TurboDocx-Signature header. Verify it before trusting the payload — the helper enforces a 300-second timestamp tolerance and uses MessageDigest.isEqual for constant-time comparison.

Java has no free functions, so the helper is exposed as WebhookSignatureVerifier.verify(...) — a static method on a final utility class. Semantically equivalent to the free-function form in JS / Py / Go / PHP.

package com.example.webhooks;

import com.turbodocx.WebhookSignatureVerifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TurboDocxWebhookController {

@Value("${turbodocx.webhook.secret}")
private String secret;

// IMPORTANT: bind to byte[], not a parsed DTO. The signature is computed
// over raw bytes — Jackson would re-serialize and whitespace mismatch
// breaks HMAC verification.
@PostMapping(value = "/webhooks/turbodocx", consumes = "application/json")
public ResponseEntity<Void> receive(
@RequestBody byte[] rawBody,
@RequestHeader("X-TurboDocx-Signature") String signature,
@RequestHeader("X-TurboDocx-Timestamp") String timestamp) {

if (!WebhookSignatureVerifier.verify(rawBody, signature, timestamp, secret)) {
return ResponseEntity.status(401).build();
}

// Now safe to parse rawBody as JSON and dispatch on event.eventType.
return ResponseEntity.ok().build();
}
}
Use the raw request body

The HMAC is computed over the exact bytes that left the TurboDocx server. Never decode JSON into a DTO and re-serialize before verifying — re-encoded JSON will not byte-match and verification will fail. In Spring, bind to @RequestBody byte[]. In a Servlet, use request.getInputStream().readAllBytes(). In JAX-RS, declare a byte[] entity parameter.

The signature contract:

FieldValue
HeaderX-TurboDocx-Signature: sha256=<hex>
Timestamp headerX-TurboDocx-Timestamp: <unix-seconds>
Signed stringtimestamp + "." + rawBody
AlgorithmHMAC-SHA256
Tolerance300 seconds (configurable via the 6-arg verify overload)
ComparisonMessageDigest.isEqual (constant-time)

Method Reference

All methods are instance methods on com.turbodocx.TurboWebhooks. Construct once via new TurboDocxClient.Builder()...buildWebhooksClient() and reuse.

createWebhook

Subscribe the org to events. Returns a JsonObject with id and secret — the secret is shown once.

JsonObject created = webhooks.createWebhook(
Arrays.asList("https://your-server.example.com/webhooks/turbodocx"),
Arrays.asList("signature.document.completed", "signature.document.voided")
);
ExceptionWhy
TurboDocxException.ConflictException (409)The signature webhook already exists for this org.
TurboDocxException.ValidationException (400)A URL is not HTTPS, or the events list is empty.
TurboDocxException.AuthorizationException (403)API key lacks the administrator role.

getWebhook

Get the org's signature webhook plus delivery statistics.

JsonObject webhook = webhooks.getWebhook();
// webhook.get("urls"), webhook.get("events"), webhook.get("isActive")
// webhook.getAsJsonObject("deliveryStats"):
// { totalDeliveries, successfulDeliveries, failedDeliveries, pendingRetries }
// webhook.get("availableEvents")

updateWebhook

Patch one or more fields. Pass null for any argument you don't want to change. Renaming is not supported.

JsonObject updated = webhooks.updateWebhook(
Arrays.asList("https://your-server.example.com/webhooks/turbodocx"), // urls
Arrays.asList("signature.document.completed"), // events
Boolean.TRUE // isActive
);

deleteWebhook

Soft-delete the webhook and its delivery history.

JsonObject deleted = webhooks.deleteWebhook();

testWebhook

Fire a synthetic delivery to every URL configured on the webhook. Useful for CI smoke tests before flipping a new receiver into production.

import java.util.LinkedHashMap;
import java.util.Map;

Map<String, Object> payload = new LinkedHashMap<>();
payload.put("documentId", "...");
payload.put("documentName", "...");

JsonObject result = webhooks.testWebhook("signature.document.completed", payload);

JsonObject summary = result.getAsJsonObject("summary");
System.out.println(summary.get("successful") + "/" + summary.get("total") + " succeeded");
if (summary.has("errors") && summary.get("errors").isJsonArray()) {
summary.getAsJsonArray("errors").forEach(err ->
System.out.println(" failure: " + err)); // per-URL failure messages
}

notifyWebhook is also exposed for symmetry with the backend surface — it routes through the same handler and returns the same shape. Prefer testWebhook in new code.

regenerateWebhookSecret

Rotate the HMAC secret. The new secret is shown once; old signatures fail immediately after rotation.

JsonObject rotated = webhooks.regenerateWebhookSecret();
String newSecret = rotated.get("secret").getAsString();
// rotated.get("regeneratedAt")

listWebhookDeliveries

Page through historical delivery attempts with filters. Pass null for any filter to skip it; the no-arg overload skips all filters.

JsonObject page = webhooks.listWebhookDeliveries(
20, // limit
null, // offset
"signature.document.completed", // eventType
Boolean.FALSE, // isDelivered
500 // httpStatus
);
// page.getAsJsonArray("results")
// page.get("totalRecords")

replayWebhookDelivery

Manually retry a past delivery by ID. Returns a freshly-created delivery row.

JsonObject replayed = webhooks.replayWebhookDelivery("delivery-uuid-here");
// replayed.get("id"), replayed.get("httpStatus"), replayed.get("attemptCount"), ...

getWebhookStats

Aggregate delivery stats over a sliding window. Pass null for the backend default (30 days).

JsonObject stats = webhooks.getWebhookStats(30);
// stats.getAsJsonObject("summary").get("successRate")
// stats.getAsJsonObject("summary").get("avgResponseTime") (milliseconds)
// stats.get("eventBreakdown") (per-event totals)

WebhookSignatureVerifier.verify (static utility)

Verify the X-TurboDocx-Signature header on an incoming request. Exposed as a static method on a final utility class — Java has no free functions, but the helper has no apiKey / orgId dependency, so it can be called from a receiver that runs in a completely different process (or deploy) than the management code.

boolean ok = WebhookSignatureVerifier.verify(
rawBody, // byte[] — raw bytes as received
signatureHeader, // value of X-TurboDocx-Signature
timestampHeader, // value of X-TurboDocx-Timestamp
webhookSecret // the secret from createWebhook
);

A String body overload is provided for convenience (verify(String rawBody, ...)), and a 6-arg overload accepts a custom toleranceSeconds plus an optional LongSupplier now for testing. Pass toleranceSeconds = 0 to disable the timestamp check entirely (NOT recommended in production).

Error Handling

import com.turbodocx.TurboDocxException;

try {
webhooks.createWebhook(urls, events);
} catch (TurboDocxException.ConflictException e) {
// 409 — signature webhook already exists; update or delete it instead
} catch (TurboDocxException.ValidationException e) {
// 400 — non-HTTPS URL, empty events list, etc.
} catch (TurboDocxException.AuthorizationException e) {
// 403 — API key lacks the administrator role
} catch (TurboDocxException.AuthenticationException e) {
// 401 — bad or revoked API key
} catch (TurboDocxException.NotFoundException e) {
// 404 — operating on a non-existent webhook
} catch (TurboDocxException.RateLimitException e) {
// 429 — back off and retry
} catch (TurboDocxException.NetworkException e) {
// request never reached the server (DNS, refused, timeout)
} catch (TurboDocxException e) {
// catch-all for any other typed SDK error (raw 5xx, etc.)
System.err.println("Error " + e.getStatusCode() + ": " + e.getMessage());
}

Common Error Codes

StatusTypeWhen
400TurboDocxException.ValidationExceptionNon-HTTPS URL, empty events, invalid body
401TurboDocxException.AuthenticationExceptionMissing or invalid API key
403TurboDocxException.AuthorizationExceptionValid key without administrator role
404TurboDocxException.NotFoundExceptionOperating on a non-existent webhook
409TurboDocxException.ConflictExceptionCreating when the signature webhook already exists
429TurboDocxException.RateLimitExceptionRate limit exceeded — back off

Runnable End-to-End Example

A complete, validated CRUD walkthrough lives in the SDK repo:

packages/java-sdk/examples/TurboWebhooksCrud.java

It exercises every CRUD step plus every error branch (400 / 401 / 403 / 404 / 409) against a live backend. Run it after exporting TURBODOCX_API_KEY and TURBODOCX_ORG_ID; override TURBODOCX_RECEIVER_URL to point at a real receiver (e.g. webhook.site, ngrok).

Gotchas

  • One webhook per org. Every method targets the fixed-name signature webhook. Creating it twice throws TurboDocxException.ConflictException (409). To manage multiple webhooks per org, call the REST API directly.
  • Save the secret immediately. createWebhook and regenerateWebhookSecret return the HMAC secret once. There is no endpoint to retrieve it later. If you lose it, rotate.
  • WebhookSignatureVerifier is a static utility — Java has no free functions, so call it as WebhookSignatureVerifier.verify(...). Final class with a private constructor; do not subclass.
  • Use the raw bytes for verification. The HMAC is over the exact request body received. In Spring, bind to @RequestBody byte[] rawBody — never Map/DTO; Jackson re-serialization breaks verification. In Servlets, use request.getInputStream().readAllBytes(). In JAX-RS, declare a byte[] entity parameter.
  • Administrator role required. The webhook routes are gated on requireOrgRole(administrator). Valid TDX- keys without the role throw TurboDocxException.AuthorizationException (403).
  • null skips fields on updateWebhook and listWebhookDeliveries. Pass null for any argument you don't want to change/filter.
  • testWebhook summary includes per-URL errors. Read result.getAsJsonObject("summary").getAsJsonArray("errors") to see exactly which receiver failed and why.
  • All TurboWebhooks methods return JsonObject. New server fields surface without an SDK upgrade — use .has(key) / .get(key) to navigate.

See Also