Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.inklink.com/llms.txt

Use this file to discover all available pages before exploring further.

InkLink can POST JSON event payloads to HTTPS URLs you control when verification results change. Delivery follows the Standard Webhooks conventions for headers and signing. Before acting on a payload, verify the signature using the exact raw request body and reject requests whose signing timestamp is outside a reasonable window to limit replay risk. Expect duplicates: the same logical event may arrive more than once (retries, slow responses, or concurrent workers), so use webhook-id and make handlers idempotent. Return 2xx quickly and move heavy work to a background queue. InkLink’s HTTP timeouts, fixed retry schedule, and API permission rules are documented below.

What you receive

Notifications are thin payloads: they include the inquiry and subject identifiers so your server can load full results via the existing KYC or Web inquiry APIs when needed.

Event types

Each type uses the form kyc.result.<status> or web.result.<status>.
typeWhen it fires
kyc.result.pendingKYC inquiry is waiting to be completed
kyc.result.manual_reviewOutcome needs manual review
kyc.result.approvedKYC approved
kyc.result.rejectedKYC rejected
kyc.result.failedKYC failed (technical or policy)
web.result.pendingWeb inquiry pending
web.result.manual_reviewOutcome needs manual review
web.result.approvedWeb inquiry approved
web.result.rejectedWeb inquiry rejected
web.result.failedWeb inquiry failed

Example payload

{
  "type": "web.result.approved",
  "timestamp": "2025-06-11T14:30:00.000Z",
  "data": {
    "inquiry_id": "web_iq_xxx",
    "subject_id": "user_123"
  }
}

Inbound request shape

When an event matches one of your endpoint’s subscribed event_types, InkLink issues an HTTP POST to your URL with:
HeaderValue
Content-Typeapplication/json
User-AgentInkLink-Webhooks/1.0
webhook-idStable id for this event (prefix wh_evt_…). Use for idempotency.
webhook-timestampUnix time (seconds) when the request was signed, as a decimal string.
webhook-signatureStandard Webhooks form: v1,<base64(HMAC-SHA256)>
The body is the raw JSON string of the payload (same bytes you must use when verifying). Redirects are not followed (fetch uses redirect: manual). Treat any non-2xx response, including 3xx, as a failed delivery.

Verifying signatures

Keep your endpoint’s signing secret (prefix whsec_) on the server only. It is returned once when you create an endpoint or rotate the secret.

Using a Standard Webhooks library

The Standard Webhooks ecosystem includes maintained reference implementations for many languages (Python, JavaScript/TypeScript, Go, Rust, and others). Prefer one of these: you pass the raw request body and the webhook headers, and the library verifies the signature (and usually enforces a timestamp tolerance). For Node.js, install standardwebhooks from npm (library source). The Webhook constructor takes the base64 key only–use the part of your InkLink secret after the whsec_ prefix as base64_secret. webhook_payload must be the raw body string (before JSON parsing); webhook_headers must include webhook-id, webhook-timestamp, and webhook-signature.
import { Webhook } from "standardwebhooks";

const wh = new Webhook(base64_secret);
wh.verify(webhook_payload, webhook_headers);

Manual verification (Node.js example)

Use this section if you are not using a library and need the exact signing procedure. Signing string (must use the exact raw request body bytes):
{webhook_id}.{webhook_timestamp}.{payload}
  • webhook_id – value of the webhook-id header
  • webhook_timestamp – value of the webhook-timestamp header (string)
  • payload – raw JSON body as a string
Compute HMAC-SHA256 using the decoded secret key (base64 portion after whsec_), Base64-encode the digest, and compare to the v1,… entry in webhook-signature using a constant-time comparison.
const crypto = require("crypto");

function verifyInkLinkWebhook(rawBody, headers, secret) {
  const webhookId = headers["webhook-id"];
  const timestamp = headers["webhook-timestamp"];
  const signatureHeader = headers["webhook-signature"];

  if (!webhookId || !timestamp || !signatureHeader || !secret?.startsWith("whsec_")) {
    return false;
  }

  const key = Buffer.from(secret.slice("whsec_".length), "base64");
  const signedContent = `${webhookId}.${timestamp}.${rawBody}`;
  const expected = crypto.createHmac("sha256", key).update(signedContent).digest("base64");

  const entries = signatureHeader.split(/\s+/);
  for (const entry of entries) {
    const [version, sig] = entry.split(",", 2);
    if (version !== "v1" || !sig) continue;
    try {
      const a = Buffer.from(sig, "base64");
      const b = Buffer.from(expected, "base64");
      if (a.length === b.length && crypto.timingSafeEqual(a, b)) return true;
    } catch {
      continue;
    }
  }
  return false;
}
Use the raw body (before JSON parsing) so the bytes match what InkLink signed. Rejecting timestamps far in the past or future limits replay risk; the Standard Webhooks spec recommends a tolerance window (libraries typically apply this for you).

Reliability and delivery attempts

Respond with 2xx quickly and offload heavy work to a background queue. InkLink records whether each HTTP attempt succeeded (status 200–299). Retries are not “wait after failure then backoff.” Each delivery uses five fixed time slots measured from that delivery’s creation time (wall-clock):
AttemptEarliest time after delivery is created
10 s
230 s
390 s
4270 s (4 min 30 s)
5720 s (12 min)
Before each attempt the worker waits until that absolute time (or runs immediately if the clock is already past the slot). Each HTTP call uses an 8 second request timeout. If all five attempts fail (non-2xx, timeout, DNS, TLS, etc.), the delivery is marked failed and is not retried further by this scheduler. Idempotency: the same logical event reuses the same webhook-id. Design handlers so duplicate deliveries (for example after a slow 2xx or overlapping workers) are safe.

Managing endpoints (API)

Use your organization’s API key on every call (Authentication). Programmatic routes: Roles: creating, updating, deleting, and rotating secrets requires an organization owner or admin. If the key’s user lacks permission, the API may respond with 500 INTERNAL_ERROR and a clear message (see Error handling). Listing and fetching endpoints is available to authenticated organization members.

Local and staging testing

Your URL must be reachable from InkLink’s servers. For local development, use a tunnel (for example ngrok, Cloudflare Tunnel, or a cloud dev environment) that terminates HTTPS with a public hostname.