API / Webhooks & errors
Webhooks
Receive a notification when each requested product finishes. Use polling as a fallback if a delivery does not arrive.
/api/v1/webhookRequest body
urlstringRequiredYour public HTTPS endpoint. Configure an account endpoint to receive a signing secret before using per-check overrides.
Manage the endpoint
| Method | Path | Behavior |
|---|---|---|
| GET | /webhook | Read the endpoint and last delivery status; never returns the secret. |
| PUT | /webhook | Set or move the endpoint. The secret is only returned on creation. |
| POST | /webhook/rotate | Issue a new signing secret. Update your receiver to use it. |
| DELETE | /webhook | Remove 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.
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.
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"));
}
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"}'
{
"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
Still have a question? We’re here to help.
