Skip to content

Alerts

An alert is the user-facing record produced when a scan run finds a hit against one of your watches. Each alert wraps exactly one Hit, the per-source verbatim record (denormalised envelope + raw payload).

Alerts are never created via the API — scan jobs running on the server are the only producer. Clients can list and read alerts. Mutating alert state (mark-read, dismiss) is in-app dashboard only in MVP; the public REST surface is read-only.

The Alert resource

{
"id": "ckalert0000000xyz",
"watchId": "ckxyz0000000abcdef",
"state": "unread",
"severity": null,
"rationale": null,
"rationaleAt": null,
"emailedAt": "2026-05-02T11:01:33.000Z",
"createdAt": "2026-05-02T11:00:00.000Z",
"hit": {
"id": "ckhit00000000aaa",
"entityType": "trademark",
"sourceCode": "global_trademark_database",
"externalId": "ST13-1500000000000",
"displayName": "ACME COSMETICS",
"ownerName": "Globex SA",
"ownerCountryCode": "FR",
"jurisdiction": "EM",
"filedAt": "2026-04-12T00:00:00.000Z",
"registeredAt": null,
"expiresAt": null,
"status": "filed",
"classes": [3, 5],
"rawPayload": {
"_source": "redacted",
"marks": [{ "st13": "1500000000000", "indexationLanguage": "en" }],
"applicants": [{ "name": "Globex SA", "address": { "country": "FR" } }]
}
}
}

Alert fields

FieldTypeNotes
idstringServer-assigned cuid().
watchIdstringThe watch this alert was raised against.
state"unread" | "read" | "dismissed"Drives the in-app unread badge. Mutable via PATCH.
severity"low" | "medium" | "high" | nullAI-scored. Always null in MVP (plan §6.1 defers AI scoring to v2).
rationalestring | nullMarkdown explanation, populated by the v2 scorer. Always null in MVP.
rationaleAtstring | nullWhen the rationale was produced.
emailedAtstring | nullWhen the alert email was sent (Solo+ tiers); null for in-app-only deliveries.
createdAtstringISO-8601.

Hit fields (alert.hit)

The hit object is denormalised onto every alert response so clients do not have to fan out a second request. It carries:

FieldTypeNotes
idstringServer-assigned.
entityTypeWatchTypeMirrors the watch’s type in MVP; kept independent so a single watch can fan out into multiple entity types in v2.
sourceCodeSourceCodeWhich adapter produced the hit (global_trademark_database in MVP).
externalIdstringSource-native primary key (e.g. an st13 identifier from the global trademark database). Stable across rescans of the same record.
displayName, ownerName, ownerCountryCode, jurisdiction, filedAt, registeredAt, expiresAt, status, classesvariousThe common envelope — denormalised from each adapter’s response so dashboards and diff jobs can query uniformly across sources.
rawPayloadobjectVerbatim source response (Prisma JSONB column). Forensic / legal use; the schema is owned by the source adapter and is not part of the API contract. Treat it as opaque.

A hit is unique per (watchId, sourceCode, externalId) — the same upstream record can legitimately appear under multiple watches but only once per watch+source.

GET /api/v1/alerts

List alerts for the calling user.

Query parameters

ParameterTypeDefaultNotes
limitinteger50Max 200.
cursorstringOpaque cursor from a previous response.
state"unread" | "read" | "dismissed"Filter by state. Repeat the param to OR multiple states.
watchIdstringRestrict to a single watch.
sincestringISO-8601 lower bound on createdAt (inclusive). Useful for incremental polling.

Required scope: alerts:read.

Example request

Terminal window
curl -sS "https://api.trademarksentinel.app/api/v1/alerts?state=unread&limit=2" \
-H "Authorization: Bearer ts_REPLACE_ME"

Example response — 200 OK

{
"data": [
{
"id": "ckalert0000000xyz",
"watchId": "ckxyz0000000abcdef",
"state": "unread",
"severity": null,
"rationale": null,
"rationaleAt": null,
"emailedAt": "2026-05-02T11:01:33.000Z",
"createdAt": "2026-05-02T11:00:00.000Z",
"hit": {
"id": "ckhit00000000aaa",
"entityType": "trademark",
"sourceCode": "global_trademark_database",
"externalId": "ST13-1500000000000",
"displayName": "ACME COSMETICS",
"ownerName": "Globex SA",
"ownerCountryCode": "FR",
"jurisdiction": "EM",
"filedAt": "2026-04-12T00:00:00.000Z",
"registeredAt": null,
"expiresAt": null,
"status": "filed",
"classes": [3, 5],
"rawPayload": { "_source": "redacted" }
}
}
],
"pagination": {
"nextCursor": null,
"limit": 2
}
}

Status codes

CodeWhen
200Success.
400Malformed query (e.g. since not an ISO-8601 timestamp).
401Missing or invalid API key.
403Tier below Team, or scope alerts:read missing.
429Rate limit exceeded.

GET /api/v1/alerts/{id}

Read a single alert. The response shape is identical to a list-row, including the embedded hit.

Required scope: alerts:read.

Example request

Terminal window
curl -sS https://api.trademarksentinel.app/api/v1/alerts/ckalert0000000xyz \
-H "Authorization: Bearer ts_REPLACE_ME"

Example response — 200 OK

{
"data": {
"id": "ckalert0000000xyz",
"watchId": "ckxyz0000000abcdef",
"state": "unread",
"severity": null,
"rationale": null,
"rationaleAt": null,
"emailedAt": "2026-05-02T11:01:33.000Z",
"createdAt": "2026-05-02T11:00:00.000Z",
"hit": {
"id": "ckhit00000000aaa",
"entityType": "trademark",
"sourceCode": "global_trademark_database",
"externalId": "ST13-1500000000000",
"displayName": "ACME COSMETICS",
"ownerName": "Globex SA",
"ownerCountryCode": "FR",
"jurisdiction": "EM",
"filedAt": "2026-04-12T00:00:00.000Z",
"registeredAt": null,
"expiresAt": null,
"status": "filed",
"classes": [3, 5],
"rawPayload": {
"_source": "redacted",
"marks": [{ "st13": "1500000000000", "indexationLanguage": "en" }]
}
}
}
}

Status codes

CodeWhen
200Success.
401Missing or invalid API key.
403Tier below Team, or scope alerts:read missing.
404The alert does not exist or is not visible to the calling key.
429Rate limit exceeded.

PATCH /api/v1/alerts/{id}

Change the alert state. No other field is mutable — to “edit” an alert, dismiss it and let the next scan re-create it.

Required scope: alerts:read. (Yes, alerts:read. There is no broader alert-write surface in MVP, and gating state changes on a separate scope would force every reader to ask for write privileges they do not need. See authentication — scopes.)

Request body

{ "state": "read" }
FieldRequiredNotes
stateyesOne of "unread", "read", "dismissed". Any other value returns 422.

Sending any field other than state returns 422 validation_failed with error.details.field naming the offending key.

Example request

Terminal window
curl -sS -X PATCH https://api.trademarksentinel.app/api/v1/alerts/ckalert0000000xyz \
-H "Authorization: Bearer ts_REPLACE_ME" \
-H "Content-Type: application/json" \
-d '{ "state": "dismissed" }'

Example response — 200 OK

{
"data": {
"id": "ckalert0000000xyz",
"watchId": "ckxyz0000000abcdef",
"state": "dismissed",
"severity": null,
"rationale": null,
"rationaleAt": null,
"emailedAt": "2026-05-02T11:01:33.000Z",
"createdAt": "2026-05-02T11:00:00.000Z",
"hit": {
"id": "ckhit00000000aaa",
"entityType": "trademark",
"sourceCode": "global_trademark_database",
"externalId": "ST13-1500000000000",
"displayName": "ACME COSMETICS",
"ownerName": "Globex SA",
"ownerCountryCode": "FR",
"jurisdiction": "EM",
"filedAt": "2026-04-12T00:00:00.000Z",
"registeredAt": null,
"expiresAt": null,
"status": "filed",
"classes": [3, 5],
"rawPayload": { "_source": "redacted" }
}
}
}

Status codes

CodeWhen
200State updated (or unchanged — idempotent).
400Malformed JSON.
401Missing or invalid API key.
403Tier below Team, or scope alerts:read missing.
404The alert does not exist or is not visible to the calling key.
422Validation failed (unknown state value, or any other field present in the body).
429Rate limit exceeded.