TurboWebhooks Java SDK
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).
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
- Maven
- Gradle
- Gradle (Kotlin DSL)
<dependency>
<groupId>com.turbodocx</groupId>
<artifactId>turbodocx-sdk</artifactId>
<version>0.2.0</version>
</dependency>
implementation 'com.turbodocx:turbodocx-sdk:0.2.0'
implementation("com.turbodocx:turbodocx-sdk:0.2.0")
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.JsonObjectfor 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_...
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());
}
}
}
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.
- Spring Boot
- Servlet
- Jakarta EE
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();
}
}
package com.example.webhooks;
import com.turbodocx.WebhookSignatureVerifier;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/webhooks/turbodocx")
public class TurboDocxWebhookServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// IMPORTANT: read raw bytes — never call getReader() or getParameter*,
// those decode and re-encode the payload and break HMAC verification.
byte[] rawBody = req.getInputStream().readAllBytes();
String signature = req.getHeader("X-TurboDocx-Signature");
String timestamp = req.getHeader("X-TurboDocx-Timestamp");
String secret = System.getenv("TURBODOCX_WEBHOOK_SECRET");
if (!WebhookSignatureVerifier.verify(rawBody, signature, timestamp, secret)) {
resp.sendError(HttpServletResponse.SC_UNAUTHORIZED, "invalid signature");
return;
}
// dispatch on event.eventType ...
resp.setStatus(HttpServletResponse.SC_OK);
}
}
package com.example.webhooks;
import com.turbodocx.WebhookSignatureVerifier;
import jakarta.ws.rs.HeaderParam;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.Response;
@Path("/webhooks/turbodocx")
public class TurboDocxWebhookResource {
// JAX-RS deserializes byte[] entities as the raw request body — exactly
// what HMAC verification needs.
@POST
public Response receive(
byte[] rawBody,
@HeaderParam("X-TurboDocx-Signature") String signature,
@HeaderParam("X-TurboDocx-Timestamp") String timestamp) {
String secret = System.getenv("TURBODOCX_WEBHOOK_SECRET");
if (!WebhookSignatureVerifier.verify(rawBody, signature, timestamp, secret)) {
return Response.status(Response.Status.UNAUTHORIZED).build();
}
// dispatch on event.eventType ...
return Response.ok().build();
}
}
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:
| Field | Value |
|---|---|
| Header | X-TurboDocx-Signature: sha256=<hex> |
| Timestamp header | X-TurboDocx-Timestamp: <unix-seconds> |
| Signed string | timestamp + "." + rawBody |
| Algorithm | HMAC-SHA256 |
| Tolerance | 300 seconds (configurable via the 6-arg verify overload) |
| Comparison | MessageDigest.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")
);
| Exception | Why |
|---|---|
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
| Status | Type | When |
|---|---|---|
| 400 | TurboDocxException.ValidationException | Non-HTTPS URL, empty events, invalid body |
| 401 | TurboDocxException.AuthenticationException | Missing or invalid API key |
| 403 | TurboDocxException.AuthorizationException | Valid key without administrator role |
| 404 | TurboDocxException.NotFoundException | Operating on a non-existent webhook |
| 409 | TurboDocxException.ConflictException | Creating when the signature webhook already exists |
| 429 | TurboDocxException.RateLimitException | Rate 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
signaturewebhook. Creating it twice throwsTurboDocxException.ConflictException(409). To manage multiple webhooks per org, call the REST API directly. - Save the secret immediately.
createWebhookandregenerateWebhookSecretreturn the HMAC secret once. There is no endpoint to retrieve it later. If you lose it, rotate. WebhookSignatureVerifieris a static utility — Java has no free functions, so call it asWebhookSignatureVerifier.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— neverMap/DTO; Jackson re-serialization breaks verification. In Servlets, userequest.getInputStream().readAllBytes(). In JAX-RS, declare abyte[]entity parameter. - Administrator role required. The webhook routes are gated on
requireOrgRole(administrator). Valid TDX- keys without the role throwTurboDocxException.AuthorizationException(403). nullskips fields onupdateWebhookandlistWebhookDeliveries. Passnullfor any argument you don't want to change/filter.testWebhooksummary includes per-URL errors. Readresult.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
- TurboSign → Webhooks — concepts, dashboard UI, retry behavior
- TurboWebhooks JavaScript / TypeScript SDK — same API, JS idioms
- TurboWebhooks Python SDK — same API, Python idioms
- TurboWebhooks Go SDK — same API, Go idioms
- TurboWebhooks PHP SDK — same API, PHP idioms
- TurboSign Java SDK — sending documents for signature
- SDKs Overview — all SDKs across all five languages
- TurboDocx SDK on GitHub