Reporting events
POST /v1/reports files a first-hand, structured report of an event that
already happened: your company closed a funding round, signed a partnership,
shipped a launch. You send the event type, when it happened, and a value for
each of the event type's roles. Evinor takes the report as submitted, with no
screening or extraction step, and publishes it under the name of the account
that owns the API key.
A report is attributed to the key owner as their own first-hand account, and it enters Evinor's event pipeline as soon as it is accepted. There is no draft state and no retraction endpoint yet. Only file facts you have confirmed yourself, and give the write scope only to integrations you trust to do that.
Six endpoints make up the reporting surface:
| Endpoint | Scope | What it does |
|---|---|---|
GET /v1/event-types | REPORTS_READ | The event types you can report, with each role's expected type. |
GET /v1/reporting-grants | REPORTS_READ | Your reporting grants: the grant_id values you can file under. |
GET /v1/entities/search?query= | REPORTS_READ | Find the entity_id to send for an entity role. |
POST /v1/reports | REPORTS_WRITE | File a report. |
GET /v1/reports | REPORTS_READ | Your filed reports, newest first (paginated). |
GET /v1/reports/{id} | REPORTS_READ | One of your reports. Returns 404 for an id that isn't your report. |
A key that files reports needs both scopes: without REPORTS_READ it
cannot look up event types or entity ids, so it cannot build a valid report.
The key dialog in Settings → API keys ticks REPORTS_READ for you when you
tick REPORTS_WRITE. See Authentication.
Reporting is not billed. It does not draw on your credit balance.
Entitlement
A scope decides which endpoints a key may call. Whether the account may actually file a report is checked separately, on every call, against the key owner's current plan:
- Filing as yourself (no
grant_id) needs structured reporting on the account. - Filing under a
grant_idneeds entity reporting on the account.
REPORTS_WRITE can only be given to a key whose owner has at least one of the
two. If the owner later loses the entitlement, the key keeps its scope, but
POST /v1/reports returns 403 not-entitled until
the entitlement is restored.
Step 1: pick an event type
GET /v1/event-types lists every event type that is currently accepting
reports. The list is not paginated. The values below are illustrative; read the
real event types from the endpoint.
{
"data": [
{
"id": "0b6f0d8e-3a7c-4d2e-9f1a-5c8e2b7d4a10",
"object": "event_type",
"code_name": "FUNDING_ROUND",
"name": "Funding round",
"description": "A company raised capital from investors.",
"roles": [
{
"code_name": "RECIPIENT",
"name": "Recipient",
"expected_type": "NAMED_ENTITY"
},
{ "code_name": "AMOUNT", "name": "Amount", "expected_type": "CURRENCY" },
{ "code_name": "ROUND", "name": "Round", "expected_type": "NONE" }
]
}
],
"has_more": false,
"next_cursor": null
}
Each role's code_name is what you send as roles[].role. Its expected_type
decides which value field you send for that role (see the table below). An
expected_type of null is a plain-text role.
Step 2: send one value per role
POST /v1/reports takes this body:
| Field | Required | Meaning |
|---|---|---|
event_type_id | yes | The id of an event type from GET /v1/event-types. |
happened_at | yes | When the event happened, as an ISO-8601 timestamp. Must not be in the future. |
roles | yes | At least one role entry, at most 50. See below. |
description | no | Free-text context, up to 5,000 characters after trimming. A blank value is dropped. |
grant_id | no | File under one of your reporting grants. |
Each entry in roles names a role by its code_name in role and carries
exactly one value field, chosen by that role's expected_type:
expected_type | Send | Example |
|---|---|---|
NAMED_ENTITY | entity_id: an entity id (UUID) | { "role": "RECIPIENT", "entity_id": "7c1e…" } |
DATE | date: a real calendar day, YYYY-MM-DD | { "role": "CLOSE_DATE", "date": "2026-09-14" } |
CURRENCY, NUMBER, DISTANCE, WEIGHT, LENGTH, AREA, VOLUME, TEMPERATURE, SPEED | amount: { "value": <number>, "unit": <text> } | { "role": "AMOUNT", "amount": { "value": 25000000, "unit": "USD" } } |
DURATION, NONE, or null | text: a non-empty string | { "role": "ROUND", "text": "Series B" } |
CATEGORICAL | Not supported yet | Any value is rejected. |
Rules that follow from the table:
- Exactly one value field per role. Sending a field that doesn't match the
role's type, or sending two, is a
400. - For
CURRENCY,unitis an ISO 4217 currency code (USD,EUR, …). For the other measurable typesunitis a unit label, and may be empty (for example, a plain count). - Send each role at most once. A role that appears twice in
rolesis a400. You don't have to send every role, but the report must carry enough of them to describe the event (seeSENTENCE_NOT_RENDERABLEunder Errors). - Durations are text (
"18 months"). A duration Evinor can't read is rejected. CATEGORICALroles can't be filed yet. Their allowed values are not published, so a report cannot fill them. Leave them out.- There is no date-time value. A
DATErole takes a calendar day only.
Entity roles take an entity id, never a name
An entity role must reference an entity that already exists in Evinor, by id. You cannot create an entity or name one in free text. This keeps a stream of name variants ("Acme", "Acme Inc.", "ACME Corp") from turning into duplicate entities.
Find the id with GET /v1/entities/search:
- curl
- TypeScript
curl "https://api.evinor.ai/v1/entities/search?query=acme&limit=5" \
-H "Authorization: Bearer evnr_live_your_key_here"
import { Evinor } from '@evinor/sdk';
const evinor = new Evinor(); // reads EVINOR_API_KEY
const { data: page } = await evinor.entities.search({ query: 'acme', limit: 5 }).firstPage();
const entityId = page.data[0]?.id;
{
"data": [
{
"id": "7c1e9a52-0d4b-4f6e-8a3c-2b9d5e1f7a64",
"object": "entity",
"name": "Acme Corporation",
"type": "ORGANIZATION"
}
],
"has_more": false,
"next_cursor": null
}
query is required (1–200 characters). The endpoint takes the usual limit
and starting_after pagination parameters. Entities that have
been merged into another entity are never returned, so an id from this endpoint
is always a current one. Evinor records the entity's name on the report for
you.
Filing a report
- curl
- TypeScript
curl -X POST https://api.evinor.ai/v1/reports \
-H "Authorization: Bearer evnr_live_your_key_here" \
-H "Idempotency-Key: 5d2f8c1a-9e7b-4c3d-a1f0-6b8e2d4c7a93" \
-H "Content-Type: application/json" \
-d '{
"event_type_id": "0b6f0d8e-3a7c-4d2e-9f1a-5c8e2b7d4a10",
"happened_at": "2026-09-14T00:00:00Z",
"roles": [
{ "role": "RECIPIENT", "entity_id": "7c1e9a52-0d4b-4f6e-8a3c-2b9d5e1f7a64" },
{ "role": "AMOUNT", "amount": { "value": 25000000, "unit": "USD" } },
{ "role": "ROUND", "text": "Series B" }
],
"description": "Led by Example Ventures."
}'
import { Evinor } from '@evinor/sdk';
const evinor = new Evinor(); // reads EVINOR_API_KEY
// Fetches the event type and checks the report against it before sending.
const builder = await evinor.reports.builder('0b6f0d8e-3a7c-4d2e-9f1a-5c8e2b7d4a10');
// Sends an Idempotency-Key (generated when you don't pass one) and retries
// under that same key.
const result = await builder
.role('RECIPIENT', { entityId: '7c1e9a52-0d4b-4f6e-8a3c-2b9d5e1f7a64' })
.role('AMOUNT', { amount: { value: 25_000_000, unit: 'USD' } })
.role('ROUND', { text: 'Series B' })
.happenedAt('2026-09-14T00:00:00Z')
.description('Led by Example Ventures.')
.submit({ idempotencyKey: '5d2f8c1a-9e7b-4c3d-a1f0-6b8e2d4c7a93' });
console.log(result.data.id);
A successful filing returns 201 with the report resource. The
TypeScript SDK also checks the report
against the event type before sending it.
The report resource
POST /v1/reports, GET /v1/reports/{id}, and each entry of GET /v1/reports
return the same shape:
{
"id": "e4a1c7b2-8d3f-4e6a-9b0c-1f2d3e4a5b6c",
"object": "report",
"status": "CONFIRMED",
"event_type_id": "0b6f0d8e-3a7c-4d2e-9f1a-5c8e2b7d4a10",
"happened_at": "2026-09-14T00:00:00.000Z",
"roles": [
{ "role": "RECIPIENT", "entity_id": "7c1e9a52-0d4b-4f6e-8a3c-2b9d5e1f7a64" },
{ "role": "AMOUNT", "amount": { "value": 25000000, "unit": "USD" } },
{ "role": "ROUND", "text": "Series B" }
],
"description": "Led by Example Ventures.",
"grant_id": null,
"published_as": { "name": "Jane Doe", "kind": "INDIVIDUAL" },
"incident_id": null,
"incident_report_id": null,
"created_at": "2026-09-25T18:02:11.412Z"
}
| Field | Meaning |
|---|---|
status | A filed report is CONFIRMED: accepted as submitted. |
roles | The same form you send. A value that hasn't been resolved to its typed form yet reads back as text. |
grant_id | The grant the report was filed under, or null. |
published_as | The public name the report is attributed to, as { name, kind }. name is your account's display name, or the grant's label for a report filed under a grant. kind is INDIVIDUAL or ORGANIZATION (filed under a grant). null for reports filed before attribution existed. |
incident_id | The event this report contributed to. null until Evinor has processed the report, which happens shortly after filing. |
incident_report_id | The report's entry in Evinor's event pipeline. null until processed. |
event_type_id and happened_at are nullable in the schema; treat a null as
unknown.
GET /v1/reports and GET /v1/reports/{id} return only reports filed through
this structured flow. GET /v1/reports/{id} answers 404
not-found for an id that doesn't exist, belongs to
another account, or isn't a structured report. All three get the same
response.
Filing under a reporting grant
A reporting grant lets an account file reports for an entity it represents,
such as an agency filing on behalf of a client. GET /v1/reporting-grants
lists yours (not paginated):
{
"data": [
{
"id": "3a9d2c7e-1b4f-4e8a-9c6d-0f5e2a7b8c1d",
"object": "reporting_grant",
"label": "Acme press office",
"status": "ACTIVE",
"is_stale": false,
"expires_at": "2027-01-01T00:00:00.000Z",
"event_type_id": "0b6f0d8e-3a7c-4d2e-9f1a-5c8e2b7d4a10",
"pins": [
{
"role_definition_id": "b2c41f7a-6e0d-4a93-8c5b-7d1e2f3a4b5c",
"role": "RECIPIENT",
"allowed_entities": [
{
"id": "7c1e9a52-0d4b-4f6e-8a3c-2b9d5e1f7a64",
"object": "entity",
"name": "Acme Corporation",
"type": "ORGANIZATION"
}
]
}
]
}
],
"has_more": false,
"next_cursor": null
}
To file under a grant, pass its id as grant_id:
- The report's
event_type_idmust be the grant'sevent_type_id. - Each pin fixes a role to a set of entities. If you send that role, its
entity_idmust be one ofallowed_entities. If a pin allows exactly one entity and you leave the role out, Evinor fills it in; if it allows several, you must send it. Anything else is refused withPIN_VIOLATION. - A grant with
is_stale: trueno longer matches its event type (for example, the event type changed after the grant was issued) and cannot be used. Its pins may carryrole: null.
The list is empty for an account without entity reporting.
Retries and idempotency
Send an Idempotency-Key with every POST /v1/reports. On
this endpoint the key does more than usual: Evinor derives the report's id from
your API key and the idempotency key, so one idempotency key always
identifies one report.
| You retry with the same key… | Result |
|---|---|
| within 24 hours of a successful first attempt | The original 201 is replayed, with Idempotency-Replayed: true. No second report. |
| while the first attempt is still running | 409 idempotency-in-flight. |
after the first attempt failed with a malformed body (400 with errors) | The key was never used. The request runs. |
| after any other failed first attempt | 409 idempotency-in-flight for about a minute, then the request runs again. |
| after the 24-hour replay window, or after a first attempt whose response you lost | 409 report-already-submitted if the report was filed. No second report. |
The last row is the protection: even when the stored response is gone, the
same key can never file the same report twice. The 409 body does not include
the report id; find the report with GET /v1/reports.
Use a new key for each genuinely new report. Without an Idempotency-Key,
every request files a new report, so a blind retry after a timeout can publish
the same report twice.
Rate limits
POST /v1/reports counts against the same per-key budget as every other REST
call, reads included: 60 requests per minute per key by default. See
Rate limits. Calls made through the MCP server draw on
the separate MCP budget instead.
Errors
A malformed body (a missing field, a non-UUID entity_id, a date not in
YYYY-MM-DD form, an unknown field) is 400
validation-failed with the usual errors
array. See Errors.
When Evinor refuses a report because of what is in it, the 400 carries a
stable code member instead, which you can branch on:
{
"type": "https://docs.evinor.ai/problems/validation-failed",
"title": "Validation failed",
"status": 400,
"detail": "Role \"AMOUNT\" expects exactly one `amount` value for its CURRENCY type.",
"request_id": "…",
"code": "ROLE_VALUE_TYPE_MISMATCH"
}
code | Meaning |
|---|---|
TEMPLATE_NOT_FOUND | No event type matches event_type_id. |
TEMPLATE_INACTIVE | The event type is not accepting reports. |
NO_ROLES | roles is empty. |
DESCRIPTION_TOO_LONG | description is over 5,000 characters after trimming. |
UNKNOWN_ROLE_CODE_NAME | A role is not one of the event type's code_names. |
ROLE_NOT_MULTI_VALUED | A role appears more than once. |
UNSUPPORTED_ROLE_TYPE | A role is CATEGORICAL. |
ROLE_VALUE_TYPE_MISMATCH | A role sends the wrong value field, or more than one. |
INVALID_DATE | A date is not a real calendar day. |
EMPTY_TEXT_VALUE | A text value is blank. |
HAPPENED_AT_INVALID | happened_at is not a valid ISO-8601 timestamp. |
HAPPENED_AT_IN_FUTURE | happened_at is in the future. |
UNKNOWN_NAMED_ENTITY | An entity_id does not exist. |
SENTENCE_NOT_RENDERABLE | The roles sent are not enough to describe this event type. Add the roles it needs. |
GRANT_STALE | The grant no longer matches the event type. |
PIN_VIOLATION | The roles don't satisfy the grant's pins. |
REPORTER_NAME_MISSING | The API key owner has no display name. Set one on your account profile first; reports filed under a grant use the grant's label instead. |
A few more codes guard size limits and internal consistency, and are listed
here so a client can recognise them: TOO_MANY_ROLES, TEXT_VALUE_TOO_LONG,
PARSED_AMOUNT_UNIT_TOO_LONG, PARSED_DATE_TOO_LONG,
PARSED_DATE_TIME_TOO_LONG, NAMED_ENTITY_ID_ON_NON_ENTITY_ROLE, and
DUPLICATE_ROLE_ID.
The other problems specific to this endpoint:
| Status | Problem | code | Meaning |
|---|---|---|---|
| 403 | not-entitled | FORBIDDEN, GRANT_FORBIDDEN, GRANT_NOT_USABLE | The account can't file reports, can't file under grants, or can't use this grant_id. The response doesn't say why a grant is unusable. |
| 409 | report-already-submitted | none | This idempotency key already filed a report. |
| 502 | upstream-error | none | The report could not be filed for another reason. Retry with the same Idempotency-Key, so a report that was filed isn't filed twice. |