iGrant.io holder notifications (REST + SSE)
When to use
Whenever a holder (wallet-side) application must react to wallet events:
an incoming credential offer, a transaction-code prompt, a front-channel
authorization step, a deferred credential becoming ready, or a verifier's
presentation request. Notifications are the holder's event channel - the
holder role has no webhooks; issuer/verifier backends use
igrantio-backend-webhooks instead. igrantio-holder-backend composes this
skill.
Before you build: run the integrator intake in igrantio-ows-overview - environment, API key, tenancy, backend host, webhooks, frontend - one question at a time, a recommended default with each.
Contract
Paths are relative to the OWS base URL; the browser calls them through the
tenant backend proxy ({backend}/ows/{tenant}/…).
| Method |
Path |
Purpose |
| GET |
v2/config/digital-wallet/openid/notifications?limit=&offset=&search=¬ificationType= |
list; the array is under the response's notification key |
| GET |
v2/config/digital-wallet/openid/notification/{id} |
read one |
| PUT |
v2/config/digital-wallet/openid/notification/{id} |
update one - body { "status": "<string>" }, response { "notification": … } |
| DELETE |
v2/config/digital-wallet/openid/notification/{id} |
delete one (the usual "handled" signal, 204) |
| DELETE |
v2/config/digital-wallet/openid/notifications |
delete all (204) |
| GET (SSE) |
v2/config/digital-wallet/openid/notifications/sse?status=unread&limit=10&offset=0&authorization=… |
live stream |
Notification item: id, notificationType, notificationContent
(object or array - take [0]), status (unread), createdAt,
updatedAt. notificationType values: credential_pending,
credential_acked, credential_revoked, credential_expired, and (SSE only)
credential_received.
Next-action decision table on notificationContent
(deriveNotificationAction in the reference client; the calls it names are in
igrantio-holder-backend/references/holder-api-reference.md):
| Action |
Condition |
Follow-up call |
transaction_code |
credentialStatus == "credential_pending" and userPinRequired and userPin absent |
PUT …/sdjwt/credential/{id}/user-pin |
authorization |
pending, acceptanceToken absent, oAuthFlow == "frontchannel", authorizationRequest set |
open authorizationRequest, then POST …/sdjwt/credential/exchange-code |
deferred_credential |
pending and acceptanceToken present |
PUT …/sdjwt/credential/{id}/receive-deferred |
verification |
pending, presentationId present, acceptanceToken absent |
POST …/sdjwt/verification/{id}/filter → …/{presentationId}/send |
review_credential |
credentialStatus == "credential_acked" |
PUT …/sdjwt/credential/{id}/accept (or DELETE to reject) |
SSE specifics:
- Auth rides in the
authorization query parameter because EventSource
cannot send headers. The gateway falls back to it when the Authorization
header is absent and accepts both prefixes: ApiKey <key> and
Bearer <jwt>. The key must not reach the browser, so the backend relay
injects it.
- Named events the server emits:
connected, notification, error
(a server-side problem report - the stream stays open), plus retry: 30000
and a keep-alive comment every ~15s. Also handle untyped messages.
notification payloads come in two shapes: a root-level
{ notificationType, notificationContent, id } or legacy
{ notification: [ … ] }. De-duplicate by id.
- Reconnect with exponential backoff: 5 attempts, 1s base, x2 per attempt,
10s cap, plus up to 500ms jitter; treat ~3 minutes of silence as stale and
reconnect. Delete the notification once acted on - deletion is the
"handled" signal.
Reference
./references:
notificationsSse.ts - notificationsSseRouter({ owsBaseUrl, getAuthorization })
Express relay; streams GET /:tenant/v2/config/digital-wallet/openid/notifications/sse.
Canonical copy - igrantio-holder-backend vendors it byte-for-byte.
notificationsClient.ts - browser types + NotificationsClient (list /
update / delete / deleteAll), openNotificationsStream (backoff + dedup),
getNotificationContent, deriveNotificationAction.
Usage
Backend (mount the relay before the proxy so one base URL serves both):
app.use(config.proxyPrefix, notificationsSseRouter({
owsBaseUrl: config.owsBaseUrl,
getAuthorization: async (tenant) => {
const key = await tenants.getApiKey(tenant);
return key ? `ApiKey ${key}` : undefined;
},
}));
app.use(config.proxyPrefix, proxyRouter(tenants, HOLDER_PERMITTED_PATHS));
Browser:
const client = new NotificationsClient("https://backend.example.com/ows/acme");
const close = openNotificationsStream({
baseUrl: "https://backend.example.com/ows/acme",
onNotification: async (n) => {
const action = deriveNotificationAction(getNotificationContent(n));
// switch (action) { … } then: await client.delete(n.id);
},
});
Adapting
- Auth scheme: if your OWS deployment authenticates SSE with user tokens,
return
Bearer <jwt> from getAuthorization - the relay passes the value
through unchanged.
- Filtering: pass
notificationType to list() to build a filtered inbox.
Validation / done criteria
npm run typecheck passes.
- The relay returns 404 for an unknown tenant and streams
text/event-stream
for a known one; the API key never appears in the browser.
- A wallet event (e.g. a credential offer received) appears on the stream and
in
list(); deleting the notification removes it from the next list().
Documentation & workflows
When anything is unclear, consult the iGrant.io documentation before guessing:
1---2name: igrantio-holder-notifications3description: Composable building block: the iGrant.io OWS holder notifications inbox - the wallet-side channel that tells a HOLDER a credential offer, transaction code, front-channel authorization, deferred credential, or presentation request needs action. REST endpoints to list and delete notifications, a live Server-Sent Events stream (auth via the authorization query parameter because EventSource cannot send headers), a backend relay that injects the key, and a dependency-free browser client with reconnect/backoff and a notification-to-next-action decision table. Composed by igrantio-holder-backend.4license: Apache-2.05---67# iGrant.io holder notifications (REST + SSE)89## When to use10Whenever a **holder** (wallet-side) application must react to wallet events:11an incoming credential offer, a transaction-code prompt, a front-channel12authorization step, a deferred credential becoming ready, or a verifier's13presentation request. Notifications are the holder's event channel - the14holder role has **no webhooks**; issuer/verifier backends use15`igrantio-backend-webhooks` instead. `igrantio-holder-backend` composes this16skill.1718**Before you build**: run the integrator intake in `igrantio-ows-overview` - environment, API key, tenancy, backend host, webhooks, frontend - one question at a time, a recommended default with each.1920## Contract2122Paths are relative to the OWS base URL; the browser calls them through the23tenant backend proxy (`{backend}/ows/{tenant}/…`).2425| Method | Path | Purpose |26| --- | --- | --- |27| GET | `v2/config/digital-wallet/openid/notifications?limit=&offset=&search=¬ificationType=` | list; the array is under the response's **`notification`** key |28| GET | `v2/config/digital-wallet/openid/notification/{id}` | read one |29| PUT | `v2/config/digital-wallet/openid/notification/{id}` | update one - body `{ "status": "<string>" }`, response `{ "notification": … }` |30| DELETE | `v2/config/digital-wallet/openid/notification/{id}` | delete one (the usual "handled" signal, 204) |31| DELETE | `v2/config/digital-wallet/openid/notifications` | delete all (204) |32| GET (SSE) | `v2/config/digital-wallet/openid/notifications/sse?status=unread&limit=10&offset=0&authorization=…` | live stream |3334**Notification item**: `id`, `notificationType`, `notificationContent`35(object **or** array - take `[0]`), `status` (`unread`), `createdAt`,36`updatedAt`. `notificationType` values: `credential_pending`,37`credential_acked`, `credential_revoked`, `credential_expired`, and (SSE only)38`credential_received`.3940**Next-action decision table** on `notificationContent`41(`deriveNotificationAction` in the reference client; the calls it names are in42`igrantio-holder-backend/references/holder-api-reference.md`):4344| Action | Condition | Follow-up call |45| --- | --- | --- |46| `transaction_code` | `credentialStatus == "credential_pending"` and `userPinRequired` and `userPin` absent | `PUT …/sdjwt/credential/{id}/user-pin` |47| `authorization` | pending, `acceptanceToken` absent, `oAuthFlow == "frontchannel"`, `authorizationRequest` set | open `authorizationRequest`, then `POST …/sdjwt/credential/exchange-code` |48| `deferred_credential` | pending and `acceptanceToken` present | `PUT …/sdjwt/credential/{id}/receive-deferred` |49| `verification` | pending, `presentationId` present, `acceptanceToken` absent | `POST …/sdjwt/verification/{id}/filter` → `…/{presentationId}/send` |50| `review_credential` | `credentialStatus == "credential_acked"` | `PUT …/sdjwt/credential/{id}/accept` (or `DELETE` to reject) |5152**SSE specifics**:53- Auth rides in the **`authorization` query parameter** because `EventSource`54 cannot send headers. The gateway falls back to it when the Authorization55 header is absent and accepts both prefixes: `ApiKey <key>` and56 `Bearer <jwt>`. The key must not reach the browser, so the backend relay57 injects it.58- Named events the server emits: `connected`, `notification`, `error`59 (a server-side problem report - the stream stays open), plus `retry: 30000`60 and a keep-alive comment every ~15s. Also handle untyped messages.61- `notification` payloads come in two shapes: a root-level62 `{ notificationType, notificationContent, id }` or legacy63 `{ notification: [ … ] }`. De-duplicate by `id`.64- Reconnect with exponential backoff: 5 attempts, 1s base, x2 per attempt,65 10s cap, plus up to 500ms jitter; treat ~3 minutes of silence as stale and66 reconnect. Delete the notification once acted on - deletion is the67 "handled" signal.6869## Reference70[`./references`](./references):71- `notificationsSse.ts` - `notificationsSseRouter({ owsBaseUrl, getAuthorization })`72 Express relay; streams `GET /:tenant/v2/config/digital-wallet/openid/notifications/sse`.73 **Canonical copy** - `igrantio-holder-backend` vendors it byte-for-byte.74- `notificationsClient.ts` - browser types + `NotificationsClient` (list /75 update / delete / deleteAll), `openNotificationsStream` (backoff + dedup),76 `getNotificationContent`, `deriveNotificationAction`.7778## Usage79Backend (mount the relay **before** the proxy so one base URL serves both):80```ts81app.use(config.proxyPrefix, notificationsSseRouter({82 owsBaseUrl: config.owsBaseUrl,83 getAuthorization: async (tenant) => {84 const key = await tenants.getApiKey(tenant);85 return key ? `ApiKey ${key}` : undefined;86 },87}));88app.use(config.proxyPrefix, proxyRouter(tenants, HOLDER_PERMITTED_PATHS));89```9091Browser:92```ts93const client = new NotificationsClient("https://backend.example.com/ows/acme");94const close = openNotificationsStream({95 baseUrl: "https://backend.example.com/ows/acme",96 onNotification: async (n) => {97 const action = deriveNotificationAction(getNotificationContent(n));98 // switch (action) { … } then: await client.delete(n.id);99 },100});101```102103## Adapting104- **Auth scheme**: if your OWS deployment authenticates SSE with user tokens,105 return `Bearer <jwt>` from `getAuthorization` - the relay passes the value106 through unchanged.107- **Filtering**: pass `notificationType` to `list()` to build a filtered inbox.108109## Validation / done criteria110- `npm run typecheck` passes.111- The relay returns 404 for an unknown tenant and streams `text/event-stream`112 for a known one; the API key never appears in the browser.113- A wallet event (e.g. a credential offer received) appears on the stream and114 in `list()`; deleting the notification removes it from the next `list()`.115116## Documentation & workflows117118When anything is unclear, consult the iGrant.io documentation before guessing:119120- iGrant.io developer APIs (index): https://docs.igrant.io/docs/developer-apis121- Getting started: https://docs.igrant.io/docs/get-started/122- OpenID4VC API (issuer / verifier / webhook): https://docs.igrant.io/docs/category/openid4vc-api/issuer