Skip to content
Browse API documentation

API / Webhooks & errors

Webhooks

Receive a notification when each requested product finishes. Use polling as a fallback if a delivery does not arrive.

PUT/api/v1/webhook

Request body

urlstringRequired

Your public HTTPS endpoint. Configure an account endpoint to receive a signing secret before using per-check overrides.

Manage the endpoint

MethodPathBehavior
GET/webhookRead the endpoint and last delivery status; never returns the secret.
PUT/webhookSet or move the endpoint. The secret is only returned on creation.
POST/webhook/rotateIssue a new signing secret. Update your receiver to use it.
DELETE/webhookRemove the account endpoint.

Handle delivery

Events are check.completed or check.failed and identify the product that finished. Combined checks can produce separate events for AI and similarity. The payload contains id, type, created_at, product, a check summary, and error.

Return a 2xx response within 10 seconds. Redirects aren't followed, and URLs that resolve to private or internal addresses aren't called. Failed deliveries are retried with backoff. Deduplicate using the event ID, and retrieve the check for its full report.

Request headers
Academicx-Event: check.completed
Academicx-Delivery: DELIVERY_ID
Academicx-Signature: t=TIMESTAMP,v1=HEX_SIGNATURE

Verify the signature

Verify the raw request body before parsing JSON. Compare the HMAC-SHA256 digest of timestamp + '.' + rawBody with v1, using your account’s signing secret. Reject malformed or stale signatures.

Node.js
import { createHmac, timingSafeEqual } from "node:crypto";
 
export function verify(rawBody, header, secret) {
  if (typeof header !== "string" || !secret) return false;
  const parts = Object.fromEntries(
    header.split(",").map(part => part.trim().split("="))
  );
  if (!/^\d+$/.test(parts.t ?? "")) return false;
  if (!/^[a-f0-9]{64}$/i.test(parts.v1 ?? "")) return false;
  const age = Math.abs(Date.now() / 1000 - Number(parts.t));
  if (!Number.isFinite(age) || age > 300) return false;
  const expected = createHmac("sha256", secret)
    .update(parts.t + "." + rawBody)
    .digest();
  return timingSafeEqual(expected, Buffer.from(parts.v1, "hex"));
}
Example request · cURL
curl -X PUT https://academi.cx/api/v1/webhook \
  -H "Authorization: Bearer $ACADEMICX_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://your-app.example/webhooks/academicx"}'
Example response · 200 · OK
{
  "object": "webhook",
  "url": "https://your-app.example/webhooks/academicx",
  "secret": "whsec_EXAMPLE_ONLY",
  "message": "Store this secret — it verifies our signature and isn't shown again."
}

Base URL: https://academi.cx/api/v1