Display Biller Messages

Production requirement

Display biller messages

A connected account is incomplete if the customer cannot see account communications delivered by its biller. Persist every message BillerAPI accepts, place it in the correct account context, and make unread messages discoverable.

Display is required; marketing permission is not implied

Display every account_update message BillerAPI accepts. Display a marketing message only after BillerAPI accepts and delivers it under explicit consent. Never bypass suppression or consent checks.

The webhook is the doorbell

Verify the signature, deduplicate the event, fetch the canonical message, upsert your projection, then acknowledge. Periodically list messages to recover anything missed or delayed.

Node
const event = billerapi.webhooks.constructEvent(rawBody, signature, webhookSecret);

if (event.type === 'customer.message.created') {
  if (await events.alreadyProcessed(event.id)) return;

  const message = await billerapi.billerMessages.retrieve(
    event.data.object.message_id,
    event.data.object.customer_user_aid,
  );

  await inbox.upsert(message.id, {
    biller: message.biller_name,
    accountLinkId: message.account_link_id,
    category: message.category,
    subject: message.subject,
    body: message.body,
    receivedAt: message.received_at,
  });
  await events.markProcessed(event.id);
}

Minimum customer experience

  • Canonical biller name and brand
  • Subject and body
  • Account-update or marketing category
  • Connected account context
  • Received time and persistent read state

A top-level inbox is optional. Account or bill context is sufficient when unread messages remain discoverable across sessions and accessible by keyboard and assistive technology.

Reconcile, read, and report

Node
for await (const message of await billerapi.billerMessages.list({
  client_user_id: 'user_123',
  account_link_id: 'link_123',
})) {
  await inbox.upsert(message.id, message);
}

await billerapi.billerMessages.markRead(messageId, 'user_123');
await billerapi.billerMessages.report(messageId, 'user_123', { reason: 'spam' });

Read and report mutations are tenant- and user-scoped. A report applies suppression only to the message's biller and category; it must not silence another biller or transactional account updates.

Production evidence

  1. Receive an account update from a connected sandbox test biller.
  2. Verify its signed webhook and fetch the canonical message.
  3. Show duplicate delivery creates one local row.
  4. Drop one webhook and recover it through list reconciliation.
  5. Persist biller/account attribution and read state.
  6. Demonstrate marketing preference and Report controls.

See also Set up webhooks and the message API reference.

Was this page helpful?