FinBaze for Platforms

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#

RoutingWhat happens
partner_deliversYou receive message.created and deliver it in your own app, push or email. The default for notifications.
finbaze_sendsFinBaze 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.
bothYou receive the event and FinBaze sends the email.
suppressNobody 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, prioritycritical, normal or digestible
recipientThe platform user (kind, account), by FinBaze id
contentReady-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
dataThe type's payload, typed as MessageData<'tax.filing_ready'> in the SDK
ack_required, ack_deadlineWhether and until when FinBaze expects an acknowledgement
dedupe_key, expires_at, createdDedupe 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 }), in company time 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 at digestHourLocal or 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.enabled is false in 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.