Skip to content

Notifications

Tenant-level alerts surfaced to users of the system. The API produces two categories of notifications:

  • Event-driven — created automatically when something happens (e.g. a document is authorized by SRI).
  • Scheduled — created or updated by the API's own background job (e.g. certificate expiry). No consumer action required.
GET   /v1/notifications
POST  /v1/notifications/:id/read
GET   /v1/notifications/preferences
PATCH /v1/notifications/preferences

See Webhooks for registering callback URLs that receive notifications in near-real time. Polling this endpoint with ?sinceId= is the fallback for consumers that cannot expose a public HTTPS callback URL.

Authentication

Authorization: Bearer <api-key> — any active key for the tenant. No X-Issuer-Id required by default; supply it to scope results to a specific issuer (see Issuer filter).


Notification object

All list and single-notification responses use the same shape:

json
{
  "id": "00000000-0000-0000-0000-000000000042",
  "type":      "DOCUMENT_AUTHORIZED",
  "severity":  "INFO",
  "title":     "Invoice authorized",
  "message":   "Invoice 001-001-000000012 for ACME Corp was authorized by SRI.",
  "metadata":  { "accessKey": "...", "sequential": "001-001-000000012", "total": "118.00" },
  "issuerId": "00000000-0000-0000-0000-000000000003",
  "readAt":    null,
  "expiresAt": null,
  "createdAt": "2026-05-28T14:30:00.000Z"
}
FieldTypeDescription
idstring (UUID)Stable identifier. Use it to deduplicate across polls and track per-user read state.
typestringMachine-readable type code — see Notification types.
severitystringINFO · WARNING · ERROR
titlestringShort human-readable headline.
messagestringFull human-readable description.
metadataobject|nullType-specific structured data (see Notification types).
issuerIdstring (UUID)|nullThe issuer this notification concerns, or null for tenant-level alerts.
readAtstring|nullISO timestamp when the notification was marked read, or null if still unread.
expiresAtstring|nullISO timestamp after which the notification should be hidden, or null if it never expires.
createdAtstringISO timestamp of creation.

Notification types

DOCUMENT_AUTHORIZED

Created automatically (fire-and-forget) inside GET /:accessKey/authorize when SRI confirms authorization. Multiple authorizations within a 60-second window are aggregated into a single row to avoid flooding the list during batch processing. The same notification id may have an updated count on successive polls within that window — the frontend should upsert by id rather than append.

A webhook payload is fired for each update to the aggregated row (including count increments).

Severity: INFO

Metadata:

json
{
  "documents": [
    {
      "accessKey": "...",
      "sequential": "001-001-000000012",
      "buyerName": "ACME Corp",
      "buyerId": "0901234567001",
      "total": "118.00",
      "issueDate": "2026-05-28",
      "authorizationNumber": "2605202615..."
    }
  ],
  "count": 5
}

documents is capped at 50 entries when a batch is large; count always reflects the true total.


CERT_EXPIRING

Created or updated by the API scheduler job when an issuer's certificate is within 30 days of its notAfter date. At most one unread row per issuer — the same row is updated in place on successive job runs (days remaining refreshes, severity may escalate). Auto-dismissed when the certificate is renewed and has > 30 days remaining.

Severity: WARNING (> 7 days) · ERROR (≤ 7 days)

Metadata:

json
{
  "issuerId": "00000000-0000-0000-0000-000000000003",
  "certExpiry": "2026-06-15T00:00:00.000Z",
  "daysRemaining": 18,
  "branchCode": "001",
  "issuePointCode": "001"
}

CERT_EXPIRED

Same conditions as CERT_EXPIRING but for a certificate whose notAfter date has already passed.

Severity: ERROR
Metadata: same shape as CERT_EXPIRING, with daysRemaining: 0.


PAYMENT_VERIFIED

Created automatically (fire-and-forget) when your provider verifies a payment proof you uploaded — covers an initial subscription, a tier-change upgrade, and a renewal uniformly; only the wording differs. A matching email is sent at the same time.

Severity: INFO

Metadata:

json
{
  "paymentId": "00000000-0000-0000-0000-000000000018",
  "subscriptionId": "00000000-0000-0000-0000-000000000012",
  "tier": "STARTER",
  "billingInterval": "MONTHLY",
  "purpose": "INITIAL",
  "amount": "20.00",
  "rejectionReasonCode": null
}

purpose is INITIAL, TIER_CHANGE, or RENEWAL. For a TIER_CHANGE payment, tier/billingInterval are the target plan being purchased, not the subscription's current one — e.g. on a payment for a monthly-STARTER-to-yearly-GROWTH change, this shows "tier": "GROWTH", "billingInterval": "YEARLY". amount is the full IVA-inclusive total (what's actually transferred via SPI), not the pre-IVA base.


PAYMENT_REJECTED

Same trigger as PAYMENT_VERIFIED, for a rejected decision instead.

Severity: WARNING

Metadata: same shape as PAYMENT_VERIFIED, with rejectionReasonCode populated (one of AMOUNT_MISMATCH, TRANSFER_NOT_FOUND, WRONG_ACCOUNT, ILLEGIBLE_PROOF, DUPLICATE_SUBMISSION, OTHER) — re-submit proof for the same paymentId via Submit Payment Proof.


SUBSCRIPTION_RENEWAL_DUE

Created automatically by the provider's scheduled job about 7 days before your subscription's current_period_end. A new RENEWAL payment is already open by the time this fires — submit proof via Submit Payment Proof using the paymentId in the metadata. A matching email includes the bank transfer instructions.

Severity: WARNING

Metadata:

json
{
  "subscriptionId": "00000000-0000-0000-0000-000000000012",
  "paymentId": "00000000-0000-0000-0000-000000000025",
  "tier": "STARTER",
  "amount": "17.39",
  "ivaAmount": "2.61",
  "totalAmount": "20.00",
  "currentPeriodEnd": "2026-07-15T00:00:00.000Z"
}

SUBSCRIPTION_PAST_DUE_WARNING

Created automatically by the same scheduled job, later in the grace period than SUBSCRIPTION_RENEWAL_DUE (by default, 5 days after current_period_end, out of a 7-day total grace period) — a more urgent notice distinct from the renewal reminder, before the account actually becomes PAST_DUE. Submit proof for the already-open RENEWAL payment via Submit Payment Proof, or start a new subscription via Create Subscription — both routes stay reachable even after the account becomes PAST_DUE (see below).

Severity: WARNING

Metadata:

json
{
  "subscriptionId": "00000000-0000-0000-0000-000000000012",
  "tier": "STARTER",
  "currentPeriodEnd": "2026-07-15T00:00:00.000Z",
  "suspendsAt": "2026-07-22T00:00:00.000Z"
}

SUBSCRIPTION_EXPIRED

Created automatically by the same scheduled job when a subscription runs about 7 days past current_period_end with no renewal ever verified. By the time this fires, the tenant has already been moved to the FREE tier and their account (GET /v1/tenants/me's status) becomes PAST_DUE — distinct from SUSPENDED: an account that can recover itself by starting a new subscription via Create Subscription and paying for it, no need to contact support. A matching email explains what happened.

Severity: ERROR

Metadata:

json
{
  "subscriptionId": "00000000-0000-0000-0000-000000000012",
  "previousTier": "STARTER"
}

PRICE_CHANGE_ANNOUNCED

Created automatically when the operator publishes a price change for a paid tier, for every active tenant still inside the 30-day notice window. This type is mandatory — it cannot be unsubscribed on any channel (see Get preferences) because it is the actual 30-day legal price-change notice, not an optional operational alert. Any renewal due before effectiveAt is still billed at the current price.

Severity: INFO

Metadata:

json
{
  "tierPriceId": "00000000-0000-0000-0000-000000000030",
  "tier": "STARTER",
  "billingInterval": "MONTHLY",
  "previousPriceUsd": 20,
  "newPriceUsd": 25,
  "effectiveAt": "2026-08-25T00:00:00.000Z"
}

Reserved types

The following types are defined in the schema and accepted by the preferences endpoint, but not yet produced by the API. They are reserved for future implementation:

TypeDescription
SRI_SUBMISSION_FAILEDSRI permanently rejected a document submission
EMAIL_DELIVERY_FAILEDMailgun reported a permanent delivery failure
QUOTA_WARNINGTenant is approaching their document quota

List notifications

GET /v1/notifications

Returns active (unexpired) notifications for the tenant, newest first. Both read and unread are included. Use readAt to decide what to show as new.

Query parameters

ParameterTypeDescription
sinceIdstring (UUID)Optional. When provided, returns only notifications with id > sinceId. Use for efficient catch-up polling: store the highest id seen on each poll and pass it on the next request.

Issuer filter

Supply X-Issuer-Id: <id> to restrict results to a specific issuer. When the header is present, the response includes:

  • Notifications whose issuerId matches the supplied value.
  • Tenant-level notifications (issuerId: null), such as future quota warnings.

Omit the header to receive all notifications across every issuer (useful for admin or overview pages).

Response

200 OK

json
{
  "notifications": [ ... ],
  "unreadCount": 3
}

Errors

StatusCodeWhen
400ISSUER_ID_INVALIDX-Issuer-Id header is present but not a valid positive integer
400ISSUER_ID_INVALIDsinceId is present but not a valid positive integer
401UNAUTHORIZEDMissing or invalid API key

Mark as read

POST /v1/notifications/:id/read

Marks a single notification as read (readAt is set to now). The notification is excluded from unreadCount on all subsequent polls.

When to call: the frontend manages per-user read state in its own database. It calls this endpoint only when every user with access to the notification has marked it read on their side. After this call the notification is considered globally read and will no longer appear in unreadCount.

Path parameters

ParameterDescription
idNotification UUID

Response

200 OK

json
{
  "notification": { ... }
}

Errors

StatusCodeWhen
400VALIDATION_FAILEDid is not a positive integer
401UNAUTHORIZEDMissing or invalid API key
404NOT_FOUNDNotification does not exist, belongs to a different tenant, or is already read

Get preferences

GET /v1/notifications/preferences

Returns the notification preference for every subscribable (type, channel) pair. Channels are IN_APP (controls whether the notification appears in GET /v1/notifications) and EMAIL (controls whether the matching email is sent, for types that have one). Pairs the tenant has never explicitly configured default to enabled: true (opt-out model).

Mandatory types (currently only PRICE_CHANGE_ANNOUNCED, the 30-day legal notice — see its section above) never appear in this list — they cannot be unsubscribed on any channel. A type with no matching email (DOCUMENT_AUTHORIZED, CERT_EXPIRING, CERT_EXPIRED) only appears with the IN_APP channel.

Response

200 OK

json
{
  "preferences": [
    { "type": "DOCUMENT_AUTHORIZED",           "channel": "IN_APP", "enabled": true },
    { "type": "CERT_EXPIRING",                 "channel": "IN_APP", "enabled": true },
    { "type": "CERT_EXPIRED",                  "channel": "IN_APP", "enabled": true },
    { "type": "PAYMENT_VERIFIED",              "channel": "IN_APP", "enabled": true },
    { "type": "PAYMENT_VERIFIED",              "channel": "EMAIL",  "enabled": true },
    { "type": "PAYMENT_REJECTED",              "channel": "IN_APP", "enabled": true },
    { "type": "PAYMENT_REJECTED",              "channel": "EMAIL",  "enabled": true },
    { "type": "SUBSCRIPTION_RENEWAL_DUE",      "channel": "IN_APP", "enabled": true },
    { "type": "SUBSCRIPTION_RENEWAL_DUE",      "channel": "EMAIL",  "enabled": true },
    { "type": "SUBSCRIPTION_PAST_DUE_WARNING", "channel": "IN_APP", "enabled": true },
    { "type": "SUBSCRIPTION_PAST_DUE_WARNING", "channel": "EMAIL",  "enabled": true },
    { "type": "SUBSCRIPTION_EXPIRED",          "channel": "IN_APP", "enabled": true },
    { "type": "SUBSCRIPTION_EXPIRED",          "channel": "EMAIL",  "enabled": true }
  ]
}

Update preferences

PATCH /v1/notifications/preferences

Bulk-upsert one or more (type, channel) preferences. Send only the pairs you want to change; unmentioned pairs are unchanged.

Request body

An array of preference objects:

json
[
  { "type": "PAYMENT_VERIFIED", "channel": "EMAIL", "enabled": false }
]
FieldTypeRequiredDescription
typestringYesOne of the subscribable (non-mandatory) notification types
channelstringYesIN_APP or EMAIL — must be a channel the type supports
enabledbooleanYestrue to enable, false to suppress

When enabled is false for (type, IN_APP), the notification stops appearing in GET /v1/notifications for that type — the row is still created internally (e.g. for webhook history), it's just filtered from the list. When enabled is false for (type, EMAIL), the matching email is not sent for that type, but the in-app notification is unaffected.

Response

200 OK — same shape as GET /v1/notifications/preferences, reflecting the full updated state.

Errors

StatusCodeWhen
400VALIDATION_FAILEDBody is not an array, or an entry has an invalid type/channel, a channel that type doesn't support, a mandatory type (cannot be unsubscribed), or a non-boolean enabled
401UNAUTHORIZEDMissing or invalid API key

┌─────────────────────────────────────────────────────────────────────┐
│  Consumer backend (e.g. Next.js)                                    │
│                                                                     │
│  Primary (near-real-time):                                          │
│    Register a webhook endpoint → receive events via POST callback   │
│    Verify X-Comprobify-Signature on each incoming request           │
│                                                                     │
│  Fallback / catch-up:                                               │
│    Poll GET /v1/notifications?sinceId=<lastSeenId> every 60–300s   │
│    Store highest id seen → pass as sinceId on next poll             │
│                                                                     │
│  When user opens notification panel:                                │
│    Mark read in frontend DB per user                                │
│    When all users have read → POST /v1/notifications/:id/read      │
└─────────────────────────────────────────────────────────────────────┘

Choosing a poll interval

The DOCUMENT_AUTHORIZED notification type aggregates multiple authorizations that occur within a 60-second window into a single row. Polling more frequently than 60 seconds would catch the row mid-aggregation — it will update again within the same window anyway, so there is no benefit to polling faster than that.

ScenarioRecommended interval
Webhooks configured (polling is fallback only)300 s (5 min) — any missed event is caught up on the next cycle
No webhooks, polling is the only delivery mechanism60 s — matches the aggregation window; going lower gives no benefit

Do not poll below 60 seconds — it will not surface new data sooner and adds unnecessary load.

Comprobify API Documentation