Skip to main content

Burgerportaal MijnOverheid Berichtenbox

The Berichtenbox bridge connects Pipelinq to the Dutch government's MijnOverheid Berichtenbox via the Logius Berichtenbox-koppelvlak (BBK) 1.7 API. It delivers zaak status updates to citizens with a DigiD-enabled mailbox, falls back to email when no mailbox is available or after 5 working days of unread state, and ingests replies as new Contactmomenten on the parent zaak.

Compliance posture​

RequirementHow the bridge meets it
Wet Modernisering Elektronisch Bestuurlijk Verkeer (WMEBV 2023)Digital-first channel for every zaak status transition.
Wet Digitale Overheid (WDO)MijnOverheid is the canonical citizen channel since 2024.
AVG art. 32 (encryption at rest)BSN encrypted with AES-256-GCM per tenant key; index lookups via HMAC-SHA256.
AVG art. 17 (right to be forgotten)BerichtenboxService::cryptoShred() wipes BSN ciphertext across Message/Reply/Resolution rows while preserving audit history.
Archiefwet 1995 + Selectielijst 2020Append-only DeliveryAuditLog rows with retentionUntil per zaak class (tenant-configurable).
BBK 1.7 (Logius binding)LogiusConnector::validateOutboundPayload() enforces UUIDv4 message id, ≤200-char subject, XHTML-strict body, ≤25 MB attachments restricted to PDF/PNG/JPG; outbound requests are RSA-SHA256 signed with the tenant PKI-overheid key.
PKIoverheidTenant key pair configured via the pki_cert / pki_key app-config keys (or the openregister key-vault).

Prerequisites​

  1. Register with Logius for a Berichtenbox client-credentials grant and note the client_id + client_secret. The Logius sandbox lets you exercise the BBK 1.7 endpoints without sending anything to real burgers.
  2. PKI-overheid certificate for the tenant organisation (one cert per gemeente). Store the PEM-encoded private key and certificate in openregister's key-vault under pipelinq:pki:<tenant>.
  3. Logius webhook secret — Logius generates a shared HMAC secret used to sign the read-receipt and inbound-reply webhooks. Store it in app config key logius_webhook_secret.

App-config keys​

KeyDefaultPurpose
tenant_iddefaultDrives per-tenant encryption/key derivation.
tenant_display_nameGemeenteSubstituted as {{gemeente}} in templates.
logius_token_urlhttps://api.logius.nl/berichtenbox/v1.7/oauth/tokenOAuth token endpoint.
logius_base_urlhttps://api.logius.nl/berichtenbox/v1.7Berichtenbox API base.
logius_client_id(empty)OAuth 2.0 client id.
logius_client_secret(empty)OAuth 2.0 client secret.
logius_webhook_secret(empty)HMAC-SHA256 webhook secret.
pki_cert(empty)PEM-encoded PKI-overheid certificate.
pki_key(empty)PEM-encoded PKI-overheid private key.
berichtenbox_fallback_from(empty)From-address for fallback emails.
selectielijst.<zaaktype>10Retention years per zaaktype for audit rows.

Templates​

Templates live in the berichtenboxTemplate OR schema. Each template is keyed by (zaaktype, status, language) and contains a Mustache-style subject + body. Available variables: {{zaakId}}, {{status}}, {{gemeente}}, {{deadline}}, {{deepLink}}, {{messageId}}. Use {{var}} for HTML-escaped substitution and {{{var}}} for raw markup (e.g. for the deep-link anchor element). The rendered body must parse as XHTML strict; the renderer wraps it in a <root> element and validates via DOMDocument.

Seed templates ship in lib/Settings/register.d/85-berichtenbox-templates.json and are imported on first run via the OR ConfigurationService::importFromApp() repair step (see [[reference_or-register-import-via-repair-step]] for why a post-migration repair step is required — occ app:enable runs migrations before peer-app autoloaders are loaded).

Endpoints​

VerbRouteAuthPurpose
POST/api/webhook/berichtenbox/readPublicPage + HMACLogius read-receipt webhook.
POST/api/webhook/berichtenbox/replyPublicPage + HMACLogius inbound-reply webhook.
POST/api/admin/berichtenbox/message/{id}/retryAdminManual retry of a failed message.
GET/api/admin/berichtenbox/statsAdminAggregate delivery counters.

The webhook routes are #[PublicPage] because Logius cannot present a Nextcloud session cookie; authenticity is enforced by HMAC-SHA256 over the raw request body with constant-time compare via hash_equals (ADR-005). The admin routes are admin-only by NC SecurityMiddleware default ([[reference_nc-security-defaults]]).

Background jobs​

ClassIntervalSpec
DispatchQueuedMessagesJob5 minREQ-OUTBOUND-001, REQ-RETRY-012
FallbackEmailJob24 hREQ-FALLBACK-004

The dispatch job processes up to 100 queued messages per run with exponential backoff (1m, 5m, 15m, 1h, 4h over 5 retries). The fallback job uses DutchHolidayCalendar to skip weekends, fixed Dutch holidays, variable Easter-derived holidays (Pasen, Paasmaandag, Hemelvaartsdag, Pinksteren, Pinkstermaandag, Goede Vrijdag), Koningsdag, kerst, and Bevrijdingsdag in lustrum years.

Monitoring​

The bridge surfaces these signals through the Nextcloud logger (level INFO for normal operations, WARNING for retryable failures, ERROR for hard failures). Operators should ship them to a central log aggregator and build alerts on the structured event field of the DeliveryAuditLog rows:

ConcernSignal
Delivery success ratecount(event=sent) / (count(event=queued)) over 1h
Failure reasonsevent=failed rows grouped by reason
Queue depthcount(BerichtenboxMessage where deliveryStatus=queued)
Fallback ratecount(event=fallback) / count(event=sent)
Stale-failedcount(BerichtenboxMessage where deliveryStatus=failed AND retryCount=5 AND updatedAt < now - 24h)

Recommended alert rules:

  • Delivery failure rate > 5 % in 1h → page on-call.
  • Queue depth > 1000 → page on-call.
  • Any message in failed state with retryCount ≥ 5 for more than 24h → ticket the operator for manual intervention.

Prometheus metric names that the next iteration of the bridge will emit (reserved here so dashboards can be built ahead of time):

  • berichtenbox_messages_dispatched_total{status} (Counter)
  • berichtenbox_messages_failed_total{reason} (Counter)
  • berichtenbox_messages_unread_days (Gauge)
  • berichtenbox_replies_received_total (Counter)
  • berichtenbox_fallback_emails_sent_total (Counter)
  • berichtenbox_dispatch_duration_seconds (Histogram)

Troubleshooting​

A message is stuck in failed — check failureReason. If the cause is transient (rate limit, network) call POST /api/admin/berichtenbox/message/ {id}/retry to re-queue; the retryCount is reset, so it gets the full backoff schedule again.

A burger says "I never got the email" but deliveryStatus = sent — the message landed in MijnOverheid; the 5-day fallback will email them automatically if they don't open it in time. If they don't have a known email on file, escalate to a manual contact.

Logius returns 401 — token endpoint URL or client credentials are wrong; verify logius_token_url, logius_client_id, logius_client_secret in app config.

Webhook returns 422 "Invalid webhook signature" — logius_webhook_secret in app config does not match the Logius-configured shared secret. Rotate both in lockstep.

No mailbox + no email — the message remains in queued (the bridge logs a WARNING with messageId). Operators must either capture the burger's email and re-queue, or contact them via post / telephone.