Skip to content

Transactional delivery

A transactional message (a receipt, a shipping notice, a status change) must be reliable and auditable. The pattern is the same for SMS and email.

  1. Send by key with a stable idempotency key

    const { data } = await fabric.messages.send("order.shipped", {
    to: "+233201234567",
    data: { name: "Ama", ref: "GH-4821" },
    reference: "order:GH-4821", // your business identifier
    idempotencyKey: "order-shipped:GH-4821", // the logical operation
    });

    Derive the idempotency key from the operation, not the attempt — if your job runs twice, the same key returns the original delivery instead of sending again.

  2. Record the delivery id

    Store data.id against your order. It is deterministic per operation, so a replay reconciles to the same record.

  3. Advance from the webhook, not the response

    // in your verified webhook handler
    if (event.type === "message.delivered") {
    await markNotified(event.data.deliveryId ?? event.data.messageId, event.data.key);
    }

    The send response means “accepted”, not “delivered”. Move your workflow forward on the terminal webhook event.

  4. Reconcile

    Compare four facts: your reference, the Fabric delivery id, the terminal event, and the wallet ledger entry. Agreement across all four is your proof of delivery and cost.

  • Use the transactional class so the message is not held by promotional quiet hours.
  • Set maxCost on the send if a runaway render could otherwise exceed an acceptable cost.
  • Handle message.failed / message.undelivered explicitly — retry through your own workflow with a new idempotency key only when a retry is genuinely warranted.