Read-only programmatic access to ESG company data, ratings, and search. Authenticate with a `cer_...` API key provisioned from your Certigence dashboard. API keys and outbound webhooks require a Professional or Enterprise subscription. Rate limits per key: 100 requests per 15 minutes on Professional, 1,000 on Enterprise; exceeding them returns HTTP 429. There are no write endpoints.
Create an API key in your Certigence dashboard and send it as Authorization: Bearer cer_... or in the X-API-Key header. Keys are tied to your company and require a Professional or Enterprise subscription; other plans receive HTTP 403. Keys are shown once and stored only as SHA-256 hashes; revoke a key in the dashboard and it stops working immediately.
Base URL: https://app.certigence.eu. All endpoints are GET requests that return JSON. /companies is paginated with page and limit (at most 100 per page) and returns a pagination object with total and totalPages; the other list endpoints take only limit, capped per endpoint (50 for search, 100 for controversies, 200 for rankings, 3,650 for score history).
curl "https://app.certigence.eu/api/v1/companies?country=Norway&limit=20" \
-H "Authorization: Bearer $CERTIGENCE_API_KEY"const response = await fetch(
"https://app.certigence.eu/api/v1/companies/123/score/history?interval=monthly",
{ headers: { "X-API-Key": process.env.CERTIGENCE_API_KEY } },
);
if (response.status === 429) {
const { retryAfter } = await response.json();
throw new Error(`Rate limited, retry in ${retryAfter}s`);
}
const { data } = await response.json();Errors are JSON with an error code and a human-readable message.
| Status | error | When it happens |
|---|---|---|
| 400 | bad_request | A parameter is invalid, for example a company ID that is not a number or a search query shorter than 2 characters. |
| 401 | unauthorized | The key is missing, malformed (it must start with cer_), unknown, revoked or expired. |
| 403 | forbidden | The key belongs to a company without a Professional or Enterprise subscription. |
| 404 | not_found | The company does not exist, or it has no published score yet. An unknown sector in /rankings returns 200 with an empty list. |
| 429 | Rate limit exceeded. … | Too many requests for this key in the current 15-minute window. The error field holds the limit message, the body carries retryAfter in seconds and the Retry-After header is set. |
| 500 | internal_error or InternalServerError | Something failed on our side; retry later. internal_error comes from key validation, InternalServerError from the endpoint itself. |
{
"error": "forbidden",
"message": "API access requires a Professional or Enterprise subscription",
"upgradeUrl": "https://certigence.eu/en/payment/subscription"
}Outbound webhooks are signed with HMAC-SHA256 using the secret shown once when the endpoint is created (or rotated). The `Certigence-Signature` header has the form `t=<unix seconds>,v1=<hex>`; v1 is `hmac_sha256(secret, "<t>.<raw body>")`. Compare in constant time and reject timestamps more than 300 seconds old. The body is `{id, type, createdAt, companyId, data}`; id is stable across retries. Event types: company.updated, company.score_changed, alert.triggered, goal.achieved, goal.at_risk, deadline.upcoming, deadline.overdue, task.completed, action_plan.completed, verification.completed, payment.succeeded, payment.failed.
Create endpoints under Webhooks in the dashboard (Professional and Enterprise). The URL must be HTTPS and reachable from the public internet; private, loopback, link-local and metadata addresses are rejected, and redirects are not followed.
Each delivery is a JSON POST with id, type, createdAt, companyId and data. The id (evt_…) stays the same on every retry, so use it to ignore duplicates. Deliveries also carry X-Webhook-Event, X-Webhook-Event-Id, X-Webhook-Delivery-Id, X-Webhook-Attempt and X-Webhook-Timestamp, plus a legacy X-Webhook-Signature (HMAC-SHA256 of the raw body without a timestamp) for receivers built before the timestamped signature.
Endpoints have 15 seconds to answer with a 2xx status. Anything else, a timeout or a redirect is a failure. Failed deliveries are retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 12 hours: 6 retries, the default. An endpoint can be set to 0 to 8 retries; retries 7 and 8 wait 24 hours each. After the last retry the delivery is dead-lettered, and you can redeliver it from the delivery log.
{
"id": "evt_1842",
"type": "goal.achieved",
"createdAt": "2026-10-25T09:30:00.000Z",
"companyId": 42,
"data": {
"goalId": 7,
"name": "Cut Scope 2 emissions by 40%",
"status": "achieved",
"previousStatus": "on_track",
"progressPercentage": 100
}
}import crypto from "node:crypto";
export function verifyCertigenceWebhook(secret, rawBody, header, toleranceSeconds = 300) {
const parts = Object.fromEntries(
header.split(",").map((part) => part.trim().split("=", 2)),
);
const timestamp = Number(parts.t);
if (!Number.isInteger(timestamp) || !parts.v1) return false;
if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
return (
expected.length === parts.v1.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
);
}