Concepts
FinBaze says things to people: a VAT return is ready, a receipt is missing, an agent needs approval. For a company on your platform your users have no FinBaze inbox, so FinBaze never emails them on its own. Each message is stored first, then routed by who should deliver it. The message catalogue lists every type.
Routing#
| Routing | What happens |
|---|---|
partner_delivers | You receive message.created and deliver it in your own app, push or email. The default for notifications. |
finbaze_sends | FinBaze emails the user. Needs a verified sender domain for your platform, so mail never shows "via FinBaze"; without one the message falls back to partner_delivers. |
both | You receive the event and FinBaze sends the email. |
suppress | Nobody sends it. It still shows in the log with the reason. |
Set a rule with platform.messages.routing.set for everything (scope: 'all'), one category or one type, for the whole platform or one company. The most specific rule wins: company type, company category, company all, platform type, platform category, platform all, then the catalogue default.
Lifecycle, billing and FinBaze account-security messages are never delivered for platform companies and cannot be changed (ROUTING_NOT_CHANGEABLE). A message of a feature you hide for the company is not sent at all.
Receiving a message#
const event = await finbaze.webhooks.constructPlatformEvent(rawBody, signature, secret);
if (isMessageEvent(event) && event.type === "message.created") {
const message = event.data.object;
if (message.type === "tax.filing_ready") {
await push(message.recipient, message.content.push); // message.data is typed for this type
}
await finbaze.platform.messages.acknowledge({ id: message.id, channel: "push" });
}
data.object is the message:
| Field | |
|---|---|
id, type, category, priority | critical, normal or digestible |
recipient | The platform user (kind, account), by FinBaze id |
content | Ready-made push (title ≤ 50, body ≤ 150), in_app (title ≤ 80, body ≤ 400, cta_label, severity), email (subject, text) and sms (≤ 160). Links are stripped from model-written text |
deep_link | { route, params }, the same routes the embedded components open |
data | The type's payload, typed as MessageData<'tax.filing_ready'> in the SDK |
ack_required, ack_deadline | Whether and until when FinBaze expects an acknowledgement |
dedupe_key, expires_at, created | Dedupe on dedupe_key; do not deliver after expires_at |
Deliveries are at least once. The same message to the same person for the same thing on the same day has the same dedupe_key.
Acknowledging#
Critical messages, and messages that serve as evidence (invoices and reminders you deliver), need an acknowledgement: messages.acknowledge({ id, channel, deliveredAt?, providerMessageId?, outcome? }), or acknowledgeMany for up to 100. It is idempotent. outcome: 'failed' tells FinBaze you could not deliver; the message then stays unacknowledged. markRead records that the user saw it.
For invoices and reminders the acknowledgement is written to the invoice's event log (partner: true, channel, your id, delivery time), so a dispute has the same trail as for a mail FinBaze sent itself.
If no acknowledgement arrives by ack_deadline (default 60 minutes for critical, 240 otherwise), FinBaze sends message.unacknowledged with the same object. With criticalEmailFallback on and a verified sender domain, FinBaze then emails a critical message itself after fallbackAfterMinutes (default 60).
Quiet hours, digests and limits#
- Critical messages are never held, digested or limited.
- Quiet hours (
settings.update({ quietHours }), incompanytime or an IANA zone) hold other messages until the window ends. - Digests. Types marked digestible are folded into one
digest.<category>message per person, daily atdigestHourLocalor hourly, when your rule asks for it or the type's default says so. - Limits. More than 20 messages per person per hour, or 3 of one type, fold into an hourly digest.
Reading the log#
platform.messages.list({ company?, status?, unacknowledgedOnly? }) shows what was sent, the routing decision and why (routingReason: catalogue_default, platform_category, company_override, hard_rule, feature_hidden, …), and the acknowledgement state. The developer portal has the same under Messages, with the routing matrix and settings.
Push notifications and the Expo sub-processor#
FinBaze sends push notifications only to FinBaze's own mobile app, for companies that are not on a platform (feature key push_notifications). A platform company's users have no FinBaze app: you deliver push yourself from message.created, and FinBaze never pushes for them.
For its own app FinBaze uses Expo's push service (exp.host) to reach Apple's and Google's push networks. Expo is therefore a sub-processor for FinBaze direct customers:
- What Expo receives. The device's push token and the notification text (title up to 50 and body up to 150 characters, plus a deep-link route). For tax deadlines and document notices the lock-screen text is generic: no amounts, names or file names.
- What it does not receive. No documents, no bookkeeping data, nothing about platform companies.
- Switch.
api.jobs.message_dispatch.push.enabledisfalsein production. It is switched on only after Expo is on the published sub-processor list, the data processing agreement with Expo is signed, and customers have had the notice period the DPA requires. Until then the in-app notification centre and email carry the messages. - Dead devices. Expo receipts are read 15 minutes after sending; a token Expo reports as unregistered is removed.
If you hold a data processing agreement with FinBaze, this changes nothing for you: platform companies never reach this path, and the sub-processor list in your agreement only changes when push is enabled for FinBaze direct customers.