---
title: "Delivery History Table"
description: "The webhook delivery history: one virtualized row per attempt with time, event type, endpoint, status and latency, plus an optional live tail."
component: true
---

```tsx
"use client"

import { useEffect, useState } from "react"
import { DeliveryHistoryTable, deliveryHistoryRowId } from "@/components/delivery-history-table"
import { fixtureLogRows } from "@/fixtures/webhook-fixtures"
import type { WebhookLogRow } from "@/lib/webhook-types"

const EVENT_TYPES = ["user.created", "user.updated", "invoice.paid", "subscription.renewed"]

/** Fabricate a live row on top of the newest one. */
const nextLiveRow = (index: number): WebhookLogRow => ({
  deliveryId: `del_live_${index}`,
  messageId: `msg_live_${index}`,
  endpointId: "ep_primary",
  type: EVENT_TYPES[index % EVENT_TYPES.length]!,
  generation: 0,
  attempt: 1,
  url: "https://api.acme.dev/webhooks/repost",
  responseStatus: index % 9 === 4 ? 500 : 200,
  latencyMs: 40 + ((index * 53) % 300),
  errorClass: null,
  timestamp: new Date().toISOString(),
})

export function DeliveryHistoryTableDemo() {
  const [selected, setSelected] = useState<WebhookLogRow | null>(null)
  const [live, setLive] = useState(true)
  const [rows, setRows] = useState<WebhookLogRow[]>(() => fixtureLogRows(60))

  // The table is presentational — this stands in for `useLogsFeed`.
  useEffect(() => {
    if (!live) return
    let index = 0
    const timer = setInterval(() => {
      index += 1
      setRows((current) => [nextLiveRow(index), ...current])
    }, 3000)
    return () => clearInterval(timer)
  }, [live])

  return (
    <DeliveryHistoryTable
      className="h-[420px] w-full"
      rows={rows}
      onRowClick={(row) => setSelected((current) => (current && deliveryHistoryRowId(current) === deliveryHistoryRowId(row) ? null : row))}
      selectedRowId={selected ? deliveryHistoryRowId(selected) : null}
      streaming={live}
      streamStatus={live ? "live" : "off"}
      onToggleStream={() => setLive((value) => !value)}
    />
  )
}

```

Built on the [Virtual Data Table](/docs/components/virtual-data-table), so the
table scrolls internally and rows render through the virtualizer. By default it
is `inverted` like the portal: newest deliveries at the visual bottom, infinite
scrollback above, and the live-tail strip underneath when `onToggleStream` is
wired.

## Installation

<ItemInstall name="delivery-history-table" target="components/delivery-history-table.tsx" />

## Usage

This component is presentational: it renders what you pass it and calls back on
interaction. In a live dashboard the data comes from `useLogsFeed() or useLogs()`
([@repost/portal-react](/docs/ui/portal-react)), but any data of
the same shape works.

<ComponentSource name="delivery-history-table-demo" title="components/delivery-history-table-demo.tsx" />

## Props

| Prop | Type | Description |
| ---- | ---- | ----------- |
| `rows` | `WebhookLogRow[]` | Attempt rows, newest first. |
| `className` | `string` | Size the frame here: the table scrolls internally (e.g. `h-[420px]`, or `min-h-0 flex-1`). |
| `isLoading` | `boolean` | Renders skeleton rows. |
| `onRowClick` | `(row) => void` | Open your detail view. |
| `selectedRowId` | `string \| null` | Highlights the active row. |
| `inverted` | `boolean` | Log-style: newest at the visual bottom. Default `true`, like the portal. |
| `onLoadOlder` / `isLoadingOlder` | — | Infinite scrollback; wire to `loadOlder` from `useLogsFeed`. |
| `streaming` / `streamStatus` / `onToggleStream` | — | The live tail: auto-stick, the "Streaming · N new" pill, and the tail strip. |
| `streamDroppedCount` / `onClearStreamDropped` | — | Surface and clear rows the stream dropped. |
| `hiddenColumns` | `string[]` | Hide columns by id: timestamp, type, url, status, latency, attempt. |
| `emptyMessage` | `ReactNode` | Shown when there are no rows. |
