Tale components

The block kinds a tale is composed from, each rendered by the same code that draws a live tale at /t/:id, beside the JSON that produces it. Copy a block, drop it into a tale's blocksarray. The field caps are the schema's; the reference lists them in full.

stationery

One envelope setting, four registers. Same grid and blocks; only paper, ink, accent and display face change. Brand overrides the accent when the owner has set a theme. Default is letter.

letter
Warm ivory, serif display. The correspondence register, and the default.
terminal
Cool paper, mono display, tighter rhythm. The engineer register.
brief
Clean near-white, grotesque sans, generous air. The consulting register.
ledger
Compact, tabular-numeral-forward. The report register.
01lead

The executive summary. Must be the first block of a real tale. text 1–600.

The double-charge in checkout is fixed, deployed and verified. One race between the payment webhook and the order writer caused it; the fix serialises them.

{
  "kind": "lead",
  "text": "The double-charge in checkout is fixed, deployed and verified. One race between the payment webhook and the order writer caused it; the fix serialises them."
}
02heading

Section title: always an <h2>, no level field. Write it as the claim. text 1–200.

The webhook and the order writer raced

{
  "kind": "heading",
  "text": "The webhook and the order writer raced"
}
03prose

Body copy in a strict markdown subset: **bold**, `code`, [links](url), flat lists. No raw HTML. markdown 1–8000.

Under load, the payment webhook and the order writer both inserted a charge row. Neither held the idempotency key, so both writes succeeded. The fix is small:

  • take the idempotency lock before the write
  • release it after the order commits

See withLock in the webhook handler.

{
  "kind": "prose",
  "markdown": "Under load, the payment webhook and the order writer both inserted a charge row. Neither held the **idempotency key**, so both writes succeeded. The fix is small:\n\n- take the idempotency lock before the write\n- release it after the order commits\n\nSee `withLock` in [the webhook handler](https://example.com/webhook)."
}
04callout

An aside with a tone: info | warning | success | note. Optional title ≤120, body 1–1000. No two in a row.

[
  {
    "kind": "callout",
    "tone": "info",
    "title": "Context",
    "body": "This tale covers the checkout double-charge only; the refund run is tracked separately."
  },
  {
    "kind": "callout",
    "tone": "warning",
    "title": "Action needed on your side",
    "body": "Rotate the webhook secret before Friday: the old one was logged in plain text."
  },
  {
    "kind": "callout",
    "tone": "success",
    "title": "Verified in production",
    "body": "Zero duplicate charges across 40k checkouts since the deploy."
  },
  {
    "kind": "callout",
    "tone": "note",
    "body": "A callout with no title is fine; the tone still sets the accent."
  }
]
05stat_tiles

The headline numbers: at most one block per tale. tiles 1–6 of { label, value, delta?, tone?: good|flat|bad }.

Since the fix
duplicate charges
0
was 41/day
p95 checkout
312 ms
refunds outstanding
3
+3 to process
orders verified
40,118
{
  "kind": "stat_tiles",
  "title": "Since the fix",
  "tiles": [
    {
      "label": "duplicate charges",
      "value": "0",
      "delta": "was 41/day",
      "tone": "good"
    },
    {
      "label": "p95 checkout",
      "value": "312 ms",
      "tone": "flat"
    },
    {
      "label": "refunds outstanding",
      "value": "3",
      "delta": "+3 to process",
      "tone": "bad"
    },
    {
      "label": "orders verified",
      "value": "40,118"
    }
  ]
}
06line_chart

One series over ordered points. title required, unit? y-suffix, points 2–120, annotations? ≤4, endLabel?. No two in a row.

Duplicate charges per day
Two-week window around the deploy
{
  "kind": "line_chart",
  "title": "Duplicate charges per day",
  "subtitle": "Two-week window around the deploy",
  "unit": "/day",
  "points": [
    {
      "label": "Mon",
      "value": 38
    },
    {
      "label": "Tue",
      "value": 41
    },
    {
      "label": "Wed",
      "value": 12
    },
    {
      "label": "Thu",
      "value": 0
    },
    {
      "label": "Fri",
      "value": 0
    }
  ],
  "annotations": [
    {
      "index": 3,
      "text": "fix deployed"
    }
  ],
  "endLabel": "0/day"
}
07table

columns 1–8 (≤60 each), rows 1–50 (each exactly columns.length cells, ≤200). Optional title.

Affected orders
OrderChargedRefunded
#48211twiceyes
#48217twiceyes
#48219twicepending
{
  "kind": "table",
  "title": "Affected orders",
  "columns": [
    "Order",
    "Charged",
    "Refunded"
  ],
  "rows": [
    [
      "#48211",
      "twice",
      "yes"
    ],
    [
      "#48217",
      "twice",
      "yes"
    ],
    [
      "#48219",
      "twice",
      "pending"
    ]
  ]
}
08code

Syntax-highlighted (Shiki). language from the enum, filename?, caption?, highlightLines? (1-indexed). code 1–8000.

src/payments/webhook.tsts
const key = idempotencyKey(event);
await withLock(key, async () => {
  await writeOrder(event);
});
The write now runs under the idempotency lock.
{
  "kind": "code",
  "language": "ts",
  "filename": "src/payments/webhook.ts",
  "code": "const key = idempotencyKey(event);\nawait withLock(key, async () => {\n  await writeOrder(event);\n});",
  "caption": "The write now runs under the idempotency lock.",
  "highlightLines": [
    2,
    3,
    4
  ]
}
09diff

Unified-diff hunks for one file. filePath, hunks 1–20 of { oldStart, oldLines, newStart, newLines, lines }; each line prefixed ' ', '+' or '-'.

src/payments/webhook.tsdiff
@@ -12,2 +12,3 @@
 const key = idempotencyKey(event);
-await writeOrder(event);
+await withLock(key, () =>
+  writeOrder(event));
{
  "kind": "diff",
  "filePath": "src/payments/webhook.ts",
  "hunks": [
    {
      "oldStart": 12,
      "oldLines": 2,
      "newStart": 12,
      "newLines": 3,
      "lines": [
        " const key = idempotencyKey(event);",
        "-await writeOrder(event);",
        "+await withLock(key, () =>",
        "+  writeOrder(event));"
      ]
    }
  ]
}
10timeline

Ordered moments. items 2–20 of { at (free label), title, body? }.

  1. 09:41
    First duplicate charge reported
    Support flagged order #48211 charged twice.
  2. 11:05
    Race identified in the webhook handler
  3. 14:20
    Fix deployed and verified
    Zero duplicates since.
{
  "kind": "timeline",
  "items": [
    {
      "at": "09:41",
      "title": "First duplicate charge reported",
      "body": "Support flagged order #48211 charged twice."
    },
    {
      "at": "11:05",
      "title": "Race identified in the webhook handler"
    },
    {
      "at": "14:20",
      "title": "Fix deployed and verified",
      "body": "Zero duplicates since."
    }
  ]
}
11before_after

Two labelled panels; each carries exactly one of markdown (≤2000) or code (≤4000, language?). label 1–60.

Before
await writeOrder(event);
After

The write runs under the idempotency lock, so a retried webhook is a no-op.

{
  "kind": "before_after",
  "before": {
    "label": "Before",
    "code": "await writeOrder(event);",
    "language": "ts"
  },
  "after": {
    "label": "After",
    "markdown": "The write runs under the idempotency lock, so a retried webhook is a no-op."
  }
}
12quote

A pulled quote. text 1–500, attribution? ≤120.

A retried webhook must be indistinguishable from the first delivery.

Payment provider integration guide
{
  "kind": "quote",
  "text": "A retried webhook must be indistinguishable from the first delivery.",
  "attribution": "Payment provider integration guide"
}
13checklist

Action items. items 1–12 of { text, owner?, due? }.

  • Rotate the webhook secretyour platform team · Fri
  • Enable idempotency alerts in the dashboardus
  • Confirm the three pending refunds cleared
{
  "kind": "checklist",
  "items": [
    {
      "text": "Rotate the webhook secret",
      "owner": "your platform team",
      "due": "Fri"
    },
    {
      "text": "Enable idempotency alerts in the dashboard",
      "owner": "us"
    },
    {
      "text": "Confirm the three pending refunds cleared"
    }
  ]
}
14evidence

The credibility layer. items 1–30 of { kind: web|pdf|doc|repo|dataset, name, url?, snippet?, note? }. snippet is the exact passage relied on.

{
  "kind": "evidence",
  "title": "What we consulted",
  "items": [
    {
      "kind": "web",
      "name": "Stripe webhook best practices",
      "url": "https://docs.stripe.com/webhooks",
      "snippet": "Webhook endpoints might occasionally receive the same event more than once."
    },
    {
      "kind": "pdf",
      "name": "PCI-DSS v4.0 §10",
      "note": "logging requirements for the secret rotation"
    },
    {
      "kind": "doc",
      "name": "Internal incident runbook",
      "note": "double-charge triage steps followed"
    },
    {
      "kind": "repo",
      "name": "payments-service",
      "url": "https://github.com/acme/payments-service",
      "note": "handler history reviewed back to the introduction of the race"
    },
    {
      "kind": "dataset",
      "name": "checkout-events-2026-07",
      "note": "40,118 orders replayed to verify zero duplicates"
    }
  ]
}
15sequence

Who calls whom, in order; drawn by the renderer as a deterministic SVG, never pixels you supply. actors 2–6 {id, label}; messages 1–30 {from, to, label, note?, reply?}; from/to must name declared actor ids; reply draws a dashed return arrow.

One charge per checkout, even on retry
BrowserCheckout servicePayment gatewayPlace orderCapture paymentcarries the idempo…Charge createdWrite the orderunder the lockRetry capturesame key, after a…Already captured,…Order confirmed
{
  "kind": "sequence",
  "title": "One charge per checkout, even on retry",
  "actors": [
    {
      "id": "browser",
      "label": "Browser"
    },
    {
      "id": "checkout",
      "label": "Checkout service"
    },
    {
      "id": "gateway",
      "label": "Payment gateway"
    }
  ],
  "messages": [
    {
      "from": "browser",
      "to": "checkout",
      "label": "Place order"
    },
    {
      "from": "checkout",
      "to": "gateway",
      "label": "Capture payment",
      "note": "carries the idempotency key"
    },
    {
      "from": "gateway",
      "to": "checkout",
      "label": "Charge created",
      "reply": true
    },
    {
      "from": "checkout",
      "to": "checkout",
      "label": "Write the order",
      "note": "under the lock"
    },
    {
      "from": "checkout",
      "to": "gateway",
      "label": "Retry capture",
      "note": "same key, after a timeout"
    },
    {
      "from": "gateway",
      "to": "checkout",
      "label": "Already captured, no-op",
      "reply": true
    },
    {
      "from": "checkout",
      "to": "browser",
      "label": "Order confirmed",
      "reply": true
    }
  ]
}
16flow

A process as a vertical flow. steps 2–12 {label, detail?, tone?: good|flat|bad, branches?}; tone tints the step edge; up to 3 branches {label, outcome} render as labelled offshoots under their step.

What happens to a webhook delivery now
  1. Webhook received
    Signature checked against the endpoint secret
  2. Idempotency key looked up
    • seen beforeacknowledged, no write
  3. Lock taken on the order row
    • not acquired in 5 sretry queued
  4. Order written, charge recorded once
{
  "kind": "flow",
  "title": "What happens to a webhook delivery now",
  "steps": [
    {
      "label": "Webhook received",
      "detail": "Signature checked against the endpoint secret"
    },
    {
      "label": "Idempotency key looked up",
      "branches": [
        {
          "label": "seen before",
          "outcome": "acknowledged, no write"
        }
      ]
    },
    {
      "label": "Lock taken on the order row",
      "tone": "flat",
      "branches": [
        {
          "label": "not acquired in 5 s",
          "outcome": "retry queued"
        }
      ]
    },
    {
      "label": "Order written, charge recorded once",
      "tone": "good"
    }
  ]
}
17mock

A proposed UI as a greyscale wireframe: unmistakably a proposal, never a screenshot (use image for what exists). frame browser|mobile|plain; regions 1–8 {role: navbar|sidebar|toolbar|content|footer, elements 1–12 {el, label?, emphasis?}}. Accent appears only on emphasised elements.

Proposed refunds view for your dashboard
Orders
3 pending
Export
Refunds outstanding
Search orders
Duplicate charges per day
#48219 awaiting your provider
Process all three
{
  "kind": "mock",
  "title": "Proposed refunds view for your dashboard",
  "frame": "browser",
  "regions": [
    {
      "role": "navbar",
      "elements": [
        {
          "el": "heading",
          "label": "Orders"
        },
        {
          "el": "badge",
          "label": "3 pending"
        },
        {
          "el": "button",
          "label": "Export"
        }
      ]
    },
    {
      "role": "sidebar",
      "elements": [
        {
          "el": "list"
        },
        {
          "el": "text"
        }
      ]
    },
    {
      "role": "content",
      "elements": [
        {
          "el": "heading",
          "label": "Refunds outstanding"
        },
        {
          "el": "input",
          "label": "Search orders"
        },
        {
          "el": "chart",
          "label": "Duplicate charges per day"
        },
        {
          "el": "card",
          "label": "#48219 awaiting your provider"
        },
        {
          "el": "button",
          "label": "Process all three",
          "emphasis": true
        }
      ]
    },
    {
      "role": "footer",
      "elements": [
        {
          "el": "text"
        }
      ]
    }
  ]
}
18divider

A hairline break between sections. No fields.


{
  "kind": "divider"
}