Skip to main content
Prerequisites: A configured integration in the ClickTerm Dashboard, a public HTTPS endpoint in your backend, and a completed signature verification flow.
Use webhooks when you want ClickTerm to notify your backend automatically after a clickwrap event has been verified. This is useful for synchronizing consent data, triggering downstream workflows, or updating internal systems without polling.

When webhooks are sent

ClickTerm sends a webhook only after your backend successfully calls POST /public-client/v1/clickwrap/verify and the clickwrap event is finalized.
Webhooks complement the verification response. Use the direct API response for the immediate user flow, and use webhooks for asynchronous backend processing.

Flow overview

Setup

1

Configure your webhook URL

Open your integration in the ClickTerm Dashboard and set a public HTTPS webhook URL for your backend.
Integrations page showing the form to add a new ClickTerm app
The URL must use HTTPS and resolve to a public IP address. Localhost, private networks (e.g. 10.x.x.x, 172.16.x.x, 192.168.x.x), and cloud metadata endpoints are rejected.
2

Store the signing secret

Save the webhook signing secret in your backend secret manager or environment configuration.
3

Expose a POST endpoint

Your endpoint must accept HTTP POST requests and preserve the raw request body for signature verification.
4

Verify before processing

Validate the timestamp and X-Clickterm-Signature header before parsing or acting on the payload.
5

Return 200 OK

Respond with 200 OK only after successful verification and processing. Any other response is treated as a failed delivery.

Request format

Webhook callbacks are sent as HTTP POST requests with Content-Type: application/json.

Headers

HeaderFormatDescription
Content-Typeapplication/jsonAlways application/json
X-Clickterm-Signaturesha256={hex_digest}HMAC SHA-256 signature prefixed with sha256=
X-Clickterm-Timestamp1711800000Unix timestamp in seconds used in the signing payload

Example raw request

POST /webhooks/clickterm HTTP/1.1
Content-Type: application/json
X-Clickterm-Timestamp: 1711800000
X-Clickterm-Signature: sha256=3f2b8a1c9d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0

{"eventType":"CLICKWRAP_EVENT_VERIFIED","data":{...}}
The X-Clickterm-Signature header always starts with the sha256= prefix followed by the hex-encoded HMAC digest. When verifying, you must either strip this prefix before comparing or prepend it to your computed digest. Comparing the raw header value directly against the hex digest alone will always fail.

Payload

{
  "eventType": "CLICKWRAP_EVENT_VERIFIED",
  "data": {
    "clickwrapEventId": "2d6cfb0f-88f0-44be-9642-68f5244a1c3d",
    "clickwrapTemplateId": "f13d7494-45ea-498f-a1a8-f7cbdf8f5fe0",
    "clickwrapTemplateVersion": 3,
    "clickwrapTemplateVersionMinor": 1,
    "endUserId": "external-user-12345",
    "templatePlaceholders": "{\"fullName\":\"TestUser\"}",
    "technicalMetadata": "{\"userAgent\":\"Mozilla/5.0\",\"ip\":\"203.0.113.42\"}",
    "actionAt": "2026-03-23T14:30:00Z",
    "effectiveAt": "2026-03-23T14:00:00Z",
    "clickwrapEventStatus": "ACCEPTED"
  }
}
templatePlaceholders and technicalMetadata are JSON-encoded strings, not objects. Parse them in your handler (e.g. JSON.parse(data.templatePlaceholders) in JavaScript, json.loads(data["templatePlaceholders"]) in Python).

Payload fields

FieldTypeDescription
eventTypeStringEvent identifier
dataObjectEvent payload. The structure depends on eventType

Event types

Currently supported webhook event types:
Event TypeDescription
CLICKWRAP_EVENT_VERIFIEDSent when a clickwrap event has been successfully verified and finalized

CLICKWRAP_EVENT_VERIFIED payload

FieldTypeDescription
clickwrapEventIdUUIDUnique identifier of the clickwrap event
clickwrapTemplateIdUUIDUnique identifier of the clickwrap template
clickwrapTemplateVersionIntegerMajor version of the template associated with the event
clickwrapTemplateVersionMinorIntegerMinor version of the template associated with the event
endUserIdStringYour end-user identifier provided during verification
templatePlaceholdersStringJSON-encoded placeholder values provided during verification, or null
technicalMetadataStringJSON-encoded technical metadata such as IP address and user agent
actionAtTimestamp (UTC, ISO-8601)When the end user accepted or declined
effectiveAtTimestamp (UTC, ISO-8601)When the template version became effective
clickwrapEventStatusStringFinal verified status, such as ACCEPTED or DECLINED

Delivery behavior

ClickTerm considers a delivery successful only when your endpoint returns 200 OK.
SettingDescription
Success conditionYour endpoint returns 200 OK after verification and processing
Failed deliveryAny non-200 response, including 201, 204, and all 4xx/5xx responses
Retry countUp to 3 retries after the initial failed attempt
Retry backoffExponential, starting at 60 seconds and increasing up to 300 seconds
Request timeout10 seconds per delivery attempt
RedirectsNot followed — your endpoint must respond directly
Only 200 OK is accepted as a success. Other 2xx status codes like 201 Created or 204 No Content are treated as failures and will trigger retries. Make sure your webhook handler returns exactly 200.
Webhook handlers should be idempotent. Retries can happen if your endpoint times out or returns a non-200 response.

Recovering missed webhooks

If all delivery attempts fail, the webhook is not retried further. Your event data is still stored in ClickTerm. To diagnose and recover:
For production integrations, combine webhooks with periodic status polling. Use webhooks for real-time event-driven workflows, and use the status endpoint as a fallback to catch any events that weren’t delivered successfully.

Verify webhook signatures

To verify a webhook:
1

Build the signing payload

Concatenate the X-Clickterm-Timestamp header, a literal dot (.), and the raw request body:
{timestamp}.{rawRequestBody}
2

Compute the HMAC

Calculate the HMAC SHA-256 digest of the signing payload using your webhook signing secret as the key, then hex-encode the result.
3

Prepend the sha256= prefix

Prepend sha256= to your computed hex digest to form the expected signature:
sha256={hex_digest}
4

Compare in constant time

Compare your expected signature against the X-Clickterm-Signature header value using a constant-time comparison function to prevent timing attacks.
@RestController
@RequestMapping("/webhooks/clickterm")
public class ClickTermWebhookController {

    private static final String TIMESTAMP_HEADER = "X-Clickterm-Timestamp";
    private static final String SIGNATURE_HEADER = "X-Clickterm-Signature";
    private static final long MAX_SKEW_SECONDS = 300L;

    @Value("${clickterm.webhook-secret}")
    private String webhookSecret;

    @PostMapping
    public ResponseEntity<Void> receiveWebhook(
        @RequestHeader(TIMESTAMP_HEADER) String timestamp,
        @RequestHeader(SIGNATURE_HEADER) String signature,
        @RequestBody byte[] rawBody
    ) throws Exception {
        long now = Instant.now().getEpochSecond();
        long requestTs = Long.parseLong(timestamp);

        if (Math.abs(now - requestTs) > MAX_SKEW_SECONDS) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        }

        String signedPayload = timestamp + "." + new String(rawBody, StandardCharsets.UTF_8);

        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(
            webhookSecret.getBytes(StandardCharsets.UTF_8),
            "HmacSHA256"
        ));

        String digest = HexFormat.of().formatHex(
            mac.doFinal(signedPayload.getBytes(StandardCharsets.UTF_8))
        );
        // Important: prepend "sha256=" to match the header format
        String expected = "sha256=" + digest;

        if (!MessageDigest.isEqual(
            signature.getBytes(StandardCharsets.UTF_8),
            expected.getBytes(StandardCharsets.UTF_8)
        )) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        }

        // Parse rawBody and process the event here.
        return ResponseEntity.ok().build();
    }
}

Best practices

  • Use the raw request body for signature verification. Do not reserialize JSON before computing the HMAC.
  • Reject stale timestamps to reduce replay risk.
  • Store webhook secrets in a secret manager or environment variables, not in source control.
  • Make event processing idempotent by keying on clickwrapEventId.
  • Return 200 OK quickly, and offload slow downstream work to a queue if needed.

Verifying a signature

Verify the ClickTerm signature before the webhook is generated.

Checking consent status

Query consent state directly for specific end users.