The Trinity Beast Infrastructure — Webhook Delivery Reporting

Every push we send you is measured, and every number we report tells you where it came from. A guide for Associates to reading delivery rate, asset coverage, and latency — and to knowing exactly how much to trust each one.

Region: us-east-2 (Ohio) API: https://api.cpmp-site.org Dashboard: https://api.cpmp-site.org/dashboard Updated: August 18, 2026

1. Overview — The Promise Behind the Numbers

When you subscribe to Webhook Push, you are buying a promise that is unusually specific: we will send you prices for the assets you selected, at the interval your tier guarantees, over the transport you chose, and we will keep doing it without you asking. Most of our engineering effort goes into keeping that promise. This document is about the other half of the work — proving we kept it, in numbers you can audit.

That distinction matters more than it might sound. A delivery system that reports on itself honestly is rarer than it should be. It is very easy to build a dashboard that shows a reassuring green number, because the easiest metrics to compute are the ones that flatter the system computing them. We have deliberately gone the other direction. Several of the numbers in your Delivery Reports are designed to make our own shortfalls visible, including one that will show you a market limitation we cannot engineer our way out of.

What you get, included with every webhook tier:
  • Delivery rate — how many of your pushes left our infrastructure successfully, split by transport
  • Asset coverage — how many of the assets you configured actually carried a fresh price
  • Coverage distribution — not just the average, but the worst push and how many were complete
  • Latency — average and 95th percentile, per day, per transport
  • Provenance — every coverage figure states whether it is a recorded fact or a reconstruction
  • 33 days of history, four export formats, and an API you can script against

None of this is an add-on. There is no reporting tier, no per-query charge, and no entitlement flag to buy. Ownership of a webhook subscription is the only gate — if you are an Associate, this is already switched on for you.

Not what you are looking for? This document covers outbound push delivery — what we sent you. If you want inbound Pull API usage — how many /price requests your key has made — that is the Listener Reporting Service, documented in LRS Report Management. The two are separate systems with separate storage and separate gating. LRS requires either a premium tier or the paid add-on; Delivery Reports do not.

How to read this document

Sections 2 through 4 explain the machinery — what we record, how we decide what constitutes a single push, and how ten million rows become an instant answer. Sections 5 through 8 define each metric precisely, including the reasoning behind definitions that could reasonably have gone another way. Section 9 is the practical reference: where to click, what to call, what every field means. Sections 10 through 12 cover language, freshness, and cost.

If you only read one section, read 6.3. It explains the single field that separates a number you can act on from a number you should treat as an estimate — and it is the field we are proudest of.

2. The Delivery Log — What We Record

Everything in your Delivery Reports is derived from one table in Aurora: webhook_delivery_log. Understanding its shape explains almost every design decision that follows, so it is worth one careful paragraph.

We write one row per asset, per transport, per push. Not one row per push. Not one row per asset. One row for every combination. If you are on Webhook Standard with 30 assets configured and delivery_method=both, a single delivery cycle writes 60 rows — 30 for the HTTPS leg and 30 for the UDP leg.

That granularity is a deliberate choice with a real cost. It means an Enterprise subscriber at 60 assets, a 3-second interval, and both transports generates on the order of 3.5 million rows per day. We accepted that cost because the alternative — recording one summary row per push — would make it impossible to answer the question Associates actually ask, which is never "did the push go out" but almost always "why is SOL missing from my feed?" You cannot answer a per-asset question from per-push data. So we store per-asset data, and we built the rest of the system to make that affordable.

2.1 What Each Row Holds

ColumnWhat it records
subscription_idYour subscription. Every query is scoped to this, always.
assetThe asset symbol, normalized to uppercase — BTC, ETH, SOL.
priceThe exact price value we put on the wire for that asset in that push.
sourceWhich of the 6 exchanges supplied the price. Useful when one venue goes thin.
delivery_methodThe transport of this legudp or https. Never both; see 3.3.
statusExactly two values: delivered or failed. There is no third state.
latency_msHow long the send took, in whole milliseconds. See section 8 for precisely what this clock covers.
attemptWhich attempt succeeded or gave up. Always 1 for UDP; 1 through 4 for HTTPS, which retries 3 times.
errorThe failure reason when there is one, and empty when there is not.
sequence_numberA counter that increments per push. Read section 3 before you rely on this — it is not what it appears to be.
configured_assetsHow many assets you had configured at the moment of that push. The most important column in the table, and the newest. Section 6.2 is entirely about it.
created_atServer-side timestamp of the write.
A note on the status vocabulary, because it has bitten us. The values are delivered and failed. They are not success and failure. If you script against this data and filter on status != 'success', every row you own will look like a failure and you will conclude you are having an outage. We know because a diagnostic query of ours did exactly that, and it briefly manufactured an incident that did not exist. Check an enum's real values before you report on them — ours are documented here precisely so you do not have to guess.

2.2 Retention — 33 Days

Raw delivery rows are kept for 33 days and then removed, oldest first, by a scheduled job inside Aurora that runs nightly at 2:30 AM Eastern. 33 days is 11 × 3, and multiples of 3 are a house convention throughout this infrastructure — you will notice it in our retention windows, batch sizes, memory allocations, and tier intervals. This window applies the same way to every subscriber on this delivery engine, whether you are a paid Associate or a free-tier Partner.

Two things follow from that window, and both are good news:

3. What Counts as One Push

Here is a question that sounds trivial and is not: given a pile of asset rows, which of them belong to the same push?

Every number in your report depends on the answer. Delivery rate is successful pushes over total pushes. Asset coverage is assets per push. Get the grouping wrong and every metric downstream is wrong — not slightly wrong, but wrong in ways that look plausible. We know the shape of that failure precisely, because we shipped it.

3.1 Why This Question Is Harder Than It Looks

Each push carries a sequence_number, and the obvious move is to group by it. That is what we did originally, and it is wrong for a reason worth understanding.

The sequence counter lives in memory inside the container that is delivering to you, and it resets to 1 whenever that container restarts. On the wire this is exactly right — you receive a clean, monotonically increasing sequence from your delivery loop, which is what lets you detect a gap. In a database spanning days and deployments, it is a disaster: after six deploys in one day, six completely unrelated pushes all carry sequence_number = 1.

Grouping by that number alone merged pushes that were hours apart. One observed group held 96 asset rows spanning four separate minutes. The consequences were not subtle:

The instinctive fix is to group by time instead — rows close together are one push. That fails too, and it fails in the opposite direction. During a rolling deploy, two containers briefly deliver to you at once, each with its own counter. Grouped by time alone, those two concurrent pushes merge into one, and we measured a single "push" containing 222 assets for a subscription configured with 30.

3.2 The Rule — Two Signals, Both Required

A push is a set of rows that share a sequence number and are time-adjacent within 1500 milliseconds. Both conditions, together. Either one alone is demonstrably broken; combined, neither failure mode is possible.

Grouping ruleResult on real production dataVerdict
Sequence number aloneAverage 37.48 assets per push against a 30-asset capInflated — impossible coverage, masked failures
Time adjacency aloneOne push containing 222 assets on a 30-asset subscriptionMerges concurrent producers during a deploy
Both, combinedAverage 27.45, maximum in any push exactly 30Correct — matches the real configured cap exactly

That third row is the confirmation that mattered. The maximum assets in any single push came out at exactly the subscription's configured limit — not near it, exactly it. A grouping rule that produces an impossible maximum is broken; one that lands precisely on the real ceiling is measuring something true.

Why 1500 milliseconds, and not a round number someone liked. The window was measured, not chosen. The widest spread we observed inside a single real push was 0.161 seconds across 29 assets — about 5.5 milliseconds per asset, which projects to roughly 0.33 seconds at our 60-asset tier ceiling. The tightest gap we observed between two pushes was 2.950 seconds, at the 3-second interval of the fastest tier we sell. So 1500 ms clears the worst projected intra-push spread by about 4.5× while sitting comfortably below the tightest cadence we offer. It is also a multiple of 3. Both bounds came from production measurement; had they overlapped, no single constant would have worked and we would have needed a different approach entirely.
Diagram 3.1 — What Counts as One Push
flowchart TB
    subgraph Rows["Raw asset rows in webhook_delivery_log"]
        R1["seq=1 · BTC · 09:00:00.000"]
        R2["seq=1 · ETH · 09:00:00.005"]
        R3["seq=1 · SOL · 09:00:00.011"]
        R4["seq=1 · BTC · 14:22:07.000"]
        R5["seq=1 · ETH · 14:22:07.006"]
    end
    Rows --> Q{"Same sequence number?"}
    Q -->|"no"| SEP["Different pushes"]
    Q -->|"yes"| T{"Gap under 1500ms
from previous row?"} T -->|"yes"| SAME["Same push"] T -->|"no"| NEW["New push — counter reset
after a container restart"] SAME --> P1["Push A — 3 assets
09:00:00"] NEW --> P2["Push B — 2 assets
14:22:07"] P1 --> OK["Coverage, delivery rate,
and latency computed per push"] P2 --> OK style P1 fill:#064e3b,stroke:#10b981,color:#d1fae5 style P2 fill:#064e3b,stroke:#10b981,color:#d1fae5 style OK fill:#1e3a5f,stroke:#60a5fa,color:#dbeafe style NEW fill:#7c2d12,stroke:#FF9900,color:#fed7aa

3.3 Two Transports Are Two Push Events

If you subscribe with delivery_method=both, one delivery cycle produces two push events in your reports — one for HTTPS, one for UDP. We do not merge them, and that is a feature rather than an accounting artifact.

The reason is that your two transports can fail completely independently, and a merged number would hide it. Here is a real subscription from our own test fleet, on a single day:

TransportPush eventsDeliveredDelivery rate
udp3,3263,326100.00%
https3,32300.00%

That subscription's HTTPS endpoint was deliberately pointed at a hostname that does not resolve, while its UDP endpoint was live. Reported per transport, the numbers tell you exactly what is wrong in one glance: your UDP feed is perfect, your HTTPS endpoint is unreachable, go check your URL. Merged into one figure, the same day would have read as roughly 50% delivery — a number that describes nothing real, points at no action, and would have you suspecting our infrastructure rather than your endpoint.

Practical consequence when reading a range summary: a subscription using both transports produces two rows per day, so a 30-day range returns about 60 rows rather than 30. The summary endpoint totals them correctly, and it also breaks the delivery rate out per transport in a by_delivery_method object — check that object first whenever an overall rate looks worse than you expected. Half your traffic being perfect and half being broken is a very different problem from everything being mildly degraded.

4. The Rollup — Ten Million Rows, Instant Answers

Your Delivery Reports are fast, and they stay fast no matter how much history accumulates. That is not because the delivery log is small — it is currently over ten million rows and more than two gigabytes. It is because your reports never read it.

Between the raw log and your report sits webhook_delivery_daily, a rollup table holding one row per subscription, per day, per transport. A month of history is about 60 rows for you. Querying it is trivially cheap, and the cost does not grow as the raw log does.

Diagram 4.1 — The Delivery Reporting Pipeline
flowchart LR
    subgraph Deliver["BeastWebhook — delivery engine"]
        LOOP["Per-subscription
delivery loop"] CACHE["WebSocket price cache
6 exchanges"] LOOP --> CACHE end subgraph Wire["Your endpoints"] HTTPS["HTTPS
signed, 3 retries"] UDP["UDP
fire and forget"] end subgraph Store["Aurora PostgreSQL"] RAW["webhook_delivery_log
one row per asset per transport
33-day retention"] DAILY["webhook_delivery_daily
one row per day per transport
permanent"] end subgraph Refresh["Rollup refresh"] CRON["pg_cron
every 15 minutes
trailing 2 days"] NIGHT["BeastReconciler
nightly, trailing 3 days
all subscriptions"] BTN["Refresh Now
on demand, today only"] end subgraph Read["What you read"] DASH["Account Dashboard
Delivery Reports panel"] API["GET /webhook/reports/*
JSON · CSV · TSV · Text"] end LOOP --> HTTPS LOOP --> UDP HTTPS --> RAW UDP --> RAW RAW --> CRON RAW --> NIGHT RAW --> BTN CRON --> DAILY NIGHT --> DAILY BTN --> DAILY DAILY --> DASH DAILY --> API style RAW fill:#7c2d12,stroke:#FF9900,color:#fed7aa style DAILY fill:#064e3b,stroke:#10b981,color:#d1fae5 style DASH fill:#1e3a5f,stroke:#60a5fa,color:#dbeafe style API fill:#1e3a5f,stroke:#60a5fa,color:#dbeafe

What the rollup computes for each day

The refresh groups your raw rows into pushes using the rule from section 3, then aggregates those pushes into one row per transport per day. Days are bucketed in America/New_York time, so a "day" in your report is an Eastern calendar day rather than a UTC one.

Stored valueHow it is computed
push_eventsCount of distinct pushes that day
successful_events / failed_eventsPushes that delivered, and pushes that did not
asset_rowsTotal asset rows across all pushes — the raw volume
avg_assets_per_pushAverage assets carried per push. The numerator of coverage.
avg_configured_per_pushPush-weighted average of the recorded configured count. The denominator of coverage, when available.
typical_assets_per_pushThe statistical mode of assets per push — the configuration the day mostly ran under
distinct_assetsThe largest single push that day, in distinct assets
min_coverage_pctThe worst single push that day, against its own configured count
pushes_at_full_coverageHow many pushes delivered every asset they had configured
avg_latency_ms / p95_latency_msAverage and 95th percentile latency across delivered pushes
last_refreshedWhen this row was last recomputed
One design detail with real consequences. The rollup writes whole-day rows, so any refresh must recompute a day from all of that day's rows. It enforces this itself: whatever start time it is given, it floors that time to the beginning of the Eastern day before reading anything. Without that floor, a refresh starting at noon would rebuild today's row from only the afternoon's pushes and overwrite the morning out of existence — a report that got less accurate every time it was updated. This is the kind of bug that never announces itself, because the resulting numbers are internally consistent and simply too low.

5. Delivery Rate — What It Actually Means

Delivery rate is the simplest metric in the report, and it is worth being precise about its scope because the precision is the useful part.

delivery_rate_pct = successful_events / push_events × 100

It is computed per day, per transport, and again across a whole range in the summary. A push counts as successful when we got the payload onto the network to your endpoint.

What this measures — and what it honestly cannot

For HTTPS, delivery rate is close to end-to-end truth. We sign the payload, POST it, and read your response. A 2xx means you accepted it. A 4xx means you rejected it and we stop immediately, because retrying a request your server has already refused is not resilience, it is noise. A 5xx or a connection failure gets retried 3 times with 1, 2, and 4 second backoff before we record a failure. So an HTTPS success genuinely means your server acknowledged the data.

For UDP, it cannot mean that, and we will not pretend otherwise. UDP is fire-and-forget by design — there is no acknowledgement in the protocol, which is exactly why it is the fastest option we offer and why latency-sensitive Associates choose it. A UDP success in your report means we wrote the datagram to the socket without error. If a router between us drops it, that packet is gone silently and no honest reporting system on our side could tell you.

This is a real limit, stated plainly rather than buried. If you need confirmed delivery, use HTTPS or use both. If you need minimum latency and can tolerate occasional loss — which is the normal trade for a price feed, where the next update is seconds away — UDP is the right choice, and the sequence field in every payload lets you detect gaps yourself. What we will not do is show you a 100% UDP delivery rate and let you infer a guarantee the protocol cannot give.

Why a partial push still counts as delivered

A push that carried 8 of your 9 assets counts as delivered, not failed. That is deliberate, and the reasoning is worth following because it is the difference between two genuinely distinct signals.

A push is one payload per transport. It either reaches your endpoint or it does not — there is no mechanism by which 8 assets arrive and the 9th is lost in transit, because they travel together in the same message. So when only 8 assets appear, the 9th did not fail to be delivered; it had no fresh price at push time and was never in the payload at all.

We verified this holds in practice rather than assuming it: across 5,814 real pushes, we found zero with mixed status. A push's asset rows always agree, because the transport succeeds or fails as a whole.

This is why we report two separate metrics instead of collapsing them into one. Delivery rate answers "is the pipe working?" Asset coverage answers "is the pipe full?" A missing asset is a price-freshness and market-liquidity signal, not a delivery signal, and folding it into delivery rate would leave you unable to tell an endpoint problem from a thin order book.

6. Asset Coverage — The Honest Metric

Asset coverage answers the question that matters most: of the assets you selected and are paying for, how many actually arrive?

asset_coverage_pct = avg_assets_per_push / coverage_denominator × 100

Two numbers, one division. The numerator is straightforward. The denominator took three attempts to get right, produced two customer-visible defects along the way, and is the reason this document exists. Both halves are worth your attention.

6.1 The Numerator — Assets Actually Delivered

avg_assets_per_push is the average number of assets carried per push that day. An asset appears in a push when our delivery engine can resolve a current price for it at that moment. When it cannot, the asset is simply absent from that push.

Price resolution has two tiers: the delivering container's own WebSocket cache, fed directly by live trades from 6 exchanges, and failing that a cluster-wide cache shared across every node. A price is eligible for webhook delivery if it is under 5 minutes old.

Why 5 minutes here, when our Pull API demands 60 seconds. These are different products and they deserve different answers. When you call /price, you are asking "what is the price right now", and serving you a four-minute-old number would be a broken promise. Webhook delivery is a subscription at a cadence you chose — as slow as 60 seconds on Starter — so a genuinely illiquid pair whose last trade was three minutes ago is better disclosed than omitted. We measured this before choosing: across the prewarm pool, price freshness was 32% at a 60-second cutoff, 54% at 5 minutes, then flattened out. Widening further would have meant serving hour-old prices to inflate a coverage number, which is the opposite of the point.

So a missing asset means one of three things, all of them information rather than malfunction: the pair is thinly traded and had no recent trade, its exchange feed is quiet, or it is genuinely illiquid. Every asset in your picker is swappable at any time in your dashboard — if an asset shows persistently low coverage, that is the system telling you to trade it for a more liquid one.

6.2 The Denominator — And Why It Was Hard

Dividing by "how many assets you configured" sounds like reading one number off your subscription. It is not, and the trap is subtle: your report covers a period of time, but your subscription row only knows what you have configured right now.

Measure a whole day's average against this moment's configuration and you get a snapshot-versus-history mismatch. Our first two attempts both failed on it, in opposite directions:

AttemptDenominator usedWhat it reportedWhy it broke
FirstAssets configured right now124.87% and 303%An Associate who ran 30 assets in the morning and 9 in the evening produced an honest 27.29 average divided by 9
SecondLargest push seen that day10.13% for a day that truly ran near 91%A brief 30-push burst at 108 assets inside an otherwise 9-asset day redefined the entire day by its ceiling
ThirdThe statistical mode91.17% — correctDescribes the configuration the day mostly operated under, so a short spike no longer rewrites it

Both early failures were correct arithmetic on a guessed number. That is the instructive part: the mathematics was never wrong, the input was an inference, and no amount of careful division fixes a denominator that was reconstructed after the fact.

The real fix: record it at the time

So we stopped inferring. Every push now records how many assets you had configured at the instant it was sent, in the configured_assets column. The denominator became a recorded fact instead of an archaeological reconstruction.

Two details make this trustworthy rather than merely present:

That second point is subtler than it looks. A recorded denominator derived even slightly differently from the delivered list would reintroduce the same class of defect one layer down — a denominator that never quite matches its own numerator. Sharing the definition is what closes it permanently.

Days that changed mid-flight

The recorded value is a push-weighted average, not a single figure, and that is precisely what makes it correct on the hardest day: one where you changed your configuration partway through.

Suppose you ran 30 assets for half a day and 9 for the other half, at 90% coverage throughout. Delivered averages 17.55; configured averages 19.5; the quotient is exactly 90%. Because both terms are push-weighted means, dividing one by the other yields total delivered over total configured — the correct aggregate ratio, with no special-casing. A single-value denominator would have picked 30 or 9 and reported 58.5% or 195%.

One thing we cannot do, said clearly. Days before this column existed cannot be backfilled — the configuration you had at a past moment is genuinely unrecoverable. Those days keep using the mode-based inference, permanently. This is why the next section exists: rather than quietly mixing facts and estimates in one column, we tell you which one you are looking at.

6.3 Coverage Basis — Every Number States Its Provenance

Every daily row carries a coverage_basis field naming where its denominator came from, and a coverage_denominator field carrying the exact value the percentage was divided by.

BasisMeaningHow much to trust it
recordedRead from configured_assets, captured at push timeA fact. Act on it directly.
typicalThe mode of delivered asset counts that dayA good inference. Reliable for stable configurations.
maxThe largest push that dayA ceiling. Understates days with brief configuration spikes.
configuredYour current configured countCorrect if you have not changed your selection during the range.
noneNothing meaningful to divide byCoverage is not applicable — e.g. a single-asset filtered view.

This field is the one we would point to if asked what distinguishes this reporting system. A metric that does not disclose its own provenance invites you to trust a reconstruction exactly as much as a measurement. The entire history of this number was numbers whose origin was unclear — so now the origin travels with the number, produced by the same function that produced the value, specifically so the two can never drift apart.

It also means you can audit us. If a coverage figure looks wrong, check the basis first: a recorded value that disagrees with your own logs is worth reporting to us as a bug. A max value that looks low on a day you changed tiers is the documented behavior described above, and the mode-based figure beside it is the better guide.

Reading the denominator itself. It is exact and unrounded. A normal day shows a whole number — 9, or 30. A day whose configuration genuinely changed mid-flight shows the true fraction, such as 19.5. We used to round it, until we noticed that rounding 19.5 up to 20 reported a real 90.00% day as 87.75% — over two points of error, landing on exactly the days a customer changed their plan. Showing the fraction is less tidy and strictly more honest, and it means the ratio printed in your report and the percentage beside it now agree exactly rather than approximately.

6.4 Why We Do Not Clamp at 100%

It would be easy to cap coverage at 100% so an impossible number can never appear again. We deliberately do not, and the reason is a principle worth stating: a wrong number that looks plausible is more dangerous than one that looks wrong.

The 124.87% reading was a gift. It was self-evidently impossible, so it got investigated within minutes and exposed a grouping defect that was also silently masking real delivery failures. Clamped to 100%, that same defect would have shown a perfectly reasonable figure and could have hidden for months.

So if you ever see coverage above 100% in your reports, please tell us. It means something is genuinely wrong, and we would very much rather hear it from you than have our own display quietly conceal it.

7. Coverage Distribution — What Averages Hide

A single average coverage figure has a structural blind spot, and it is a serious one. Consider two days that both report 87.50% coverage:

Identical headline. Completely different problems. Day A is a market-liquidity ceiling — some of your assets are thinly traded and the average reflects it. Day B is an intermittent fault, and the two demand opposite responses: Day A means reconsider your asset selection, Day B means look at what happened at those specific moments.

An average cannot distinguish them. Not because ours is poorly implemented, but because averaging is by definition the operation that discards distribution. So we report two additional numbers alongside it, and they are designed to be read together.

FieldWhat it tells youCharacter
pushes_at_full_coverageHow many pushes that day delivered every asset they had configuredRobust. A count — one bad push cannot move it meaningfully.
min_coverage_pctThe worst single push that day, against its own configured countAn extreme, by design. Answers "how bad did it get".

Applied to the two days above, the ambiguity disappears immediately. Day A reports few or no full pushes and a minimum close to its average. Day B reports mostly full pushes and a minimum of 30%. One glance, two different stories, two different actions.

Why two fields instead of one clever one. Each answers half the question and neither is sufficient alone. min_coverage_pct is deliberately sensitive to a single event — a container restarting with a cold price cache will produce one thin push and land here, which is exactly what you want when hunting an incident and exactly what you do not want as a summary of the day. pushes_at_full_coverage is the opposite: stable, unmoved by one outlier, useless for finding the outlier. Together they read as a plain sentence — "2,900 of 2,983 pushes complete, worst push 33%" — and that sentence is actionable in a way that no single number is.

The insight this made possible

These two fields immediately taught us something about our own product that the average had been hiding. A subscription can report healthy average coverage — comfortably in the 90s — while delivering its complete configured set on only a minority of pushes. It consistently delivers nearly everything, and rarely everything.

Whether that matters depends entirely on what you are building. If you are computing a portfolio value or driving a display, near-complete every few seconds is fine. If your logic requires all N assets present in the same message before it can act, you should be watching pushes_at_full_coverage, not average coverage — and before these fields existed, there was no way for you to know the difference.

The honest framing is this: the constraint is real trading volume on real exchanges, not our engineering. As you configure more assets, the chance that every single one has a fresh price at the same instant falls, because you are necessarily reaching further down the liquidity curve. A tighter selection of well-traded assets will deliver complete pushes far more consistently than a broad selection reaching into thin markets. These fields let you measure that trade-off on your own subscription instead of taking our word for it.

7.1 Why Our Top Tier Stops at 60 Assets

This is the same measurement that set our pricing, so it is worth showing you the number it turns on. Across the 150+ assets we prewarm on 6 exchanges, here is how 24-hour trading volume actually distributes:

24-hour volumeAssets
$100M or more2
$10M or more9
$1M or more65
$100K or more211
$10K or more270
under $10K87

Only about 65 assets trade above a million dollars a day. That is the real ceiling on how many assets can carry a genuinely fresh price at a 3-second cadence, and it is why both Professional and Enterprise cap at 60 rather than something larger.

We think that decision is worth explaining rather than just presenting, because the alternative was available and we rejected it. An earlier version of the Enterprise tier advertised 150 assets. It was never deliverable in full — measured delivery sat around 70 of 150 — for the simple reason that a 150-asset selection has to reach roughly 85 assets past the liquid universe. Rather than keep a number that reads well and cannot be met, we brought the ceiling down to what the market actually supports and made the top tier differentiate on speed instead: same 60 assets, pushed twice as often.

What this means for how you choose. A fast interval is only meaningful on a liquid asset — pushing a thinly-traded token every 3 seconds implies a freshness the market cannot supply, and you would mostly receive the same price repeatedly. So the useful strategy is not "fill every slot." It is: select the assets you genuinely need, weight toward liquid ones, and watch pushes_at_full_coverage to see whether your particular selection delivers completely. If it does not, the picker sorts by volume and every asset is swappable at any time.
Availability note. Both fields depend on the recorded configured count, so they populate only for days where coverage_basis is recorded. Where they cannot be computed they render as a dash rather than a zero — a zero would read as catastrophic failure, and "not measured" is the truth. In range summaries they are paired with pushes_coverage_measured, which states how many pushes the figures actually cover, so the pair reports its own scope rather than implying it spans everything.

8. Latency — What We Measure, and What We Cannot

Each day and transport reports two latency figures, both in milliseconds: avg_latency_ms and p95_latency_ms. Both are computed across delivered pushes only — a failed push's timing measures how long a failure took, which would quietly poison the number you actually care about.

Precisely what the clock covers

TransportClock startsClock stopsIncludes
httpsBefore we sign the payloadWhen your response is fully receivedHMAC signing, connection, transfer, your server's processing time, and any retry backoff
udpBefore the socket is openedWhen the datagram is writtenSocket setup and the write itself

Two consequences follow, and both are deliberate:

Why p95 and not just the average. An average is easily dominated by the common case and will look excellent while a meaningful slice of your pushes are slow. The 95th percentile answers a different and often more useful question: how slow is a bad-but-not-rare delivery? If your average is 15 ms and your p95 is 49 ms, you have a well-behaved feed with a modest tail. If your average is 15 ms and your p95 is 900 ms, something intermittent is happening that the average is concealing — and it is almost always worth finding.

Latency is recorded per push leg rather than per asset. Every asset row within one push carries the same value, because the payload travelled as a single message — there is no per-asset timing to record, and inventing one would be fiction.

Note that range summaries report an average but no p95. Percentiles cannot be meaningfully averaged across days — a p95 of p95s is not a p95 of anything — so rather than compute a number that would look authoritative and mean nothing, we omit it. For tail latency across a range, read the daily rows.

9. Reading Your Reports

Two ways in, backed by the same data and the same arithmetic: the Account Dashboard for looking, the API for scripting. They cannot disagree, because they resolve every number through shared logic — a deliberate choice after we learned how easily two implementations of one metric drift into two different answers.

9.1 In the Account Dashboard

Sign in at api.cpmp-site.org/dashboard and open Delivery Reports. Login is passwordless — request a magic link, click it, you are in.

ControlWhat it does
Start / End dateBounds the range. Defaults to the trailing 30 days.
MethodFilter to HTTPS or UDP alone, or leave blank for both.
AssetNarrow to one asset. Reads the raw log, so it is bounded to 33 days and reports no coverage — see the note below.
Run ReportLoads the summary and the day-by-day breakdown.
Refresh NowRecomputes today's rollup immediately. Limited to once per 60 seconds.
JSON / CSV / TSV / TextExport the current view. Same four formats the API offers — the dashboard gets no more and no fewer.

There is also a separate Delivery Log panel for live activity: your 50 most recent pushes, expandable to the individual asset prices in each, with second-precision Eastern timestamps. Use Delivery Log for "what is happening right now" and Delivery Reports for "how has this performed". At Enterprise's 3-second interval, second precision in the log is not a nicety — minute-rounded timestamps would collapse about twenty distinct pushes into one apparent moment.

The asset filter deliberately reports no coverage. When you filter to a single asset, coverage_basis comes back as none and coverage is blank. That is correct, not missing: one asset measured against your whole subscription's configured count would produce a meaningless percentage — a subscription with 30 assets filtered to BTC would read about 3% coverage and look catastrophic while nothing whatsoever is wrong. We would rather show you nothing than something confidently wrong.

9.2 Over the API

Three endpoints. Authentication is your webhook API key, presented as an X-API-Key header, an Authorization: Bearer header, or an api_key query parameter. No other entitlement is required — owning the subscription is the whole gate.

MethodEndpointReturns
GET/webhook/reports/dailyOne row per day per transport
GET/webhook/reports/summaryRange totals plus a per-transport breakdown
POST/webhook/reports/refreshStarts an on-demand rollup recompute — returns 202 immediately, does not wait for the recompute to finish
GET/webhook/reports/refresh/statusPoll this to learn when the refresh you just started has finished

Query parameters

ParameterFormatDefault
start_dateYYYY-MM-DD30 days ago
end_dateYYYY-MM-DDToday
methodhttps or udpBoth
assetAsset symbol, case-insensitiveAll assets
formatcsv, tsv, textJSON

Examples

# Last 30 days, day by day
curl -H "X-API-Key: $WEBHOOK_KEY" \
  "https://api.cpmp-site.org/webhook/reports/daily"

# A specific month, HTTPS only, as CSV
curl -H "X-API-Key: $WEBHOOK_KEY" \
  "https://api.cpmp-site.org/webhook/reports/daily?start_date=2026-07-01&end_date=2026-07-31&method=https&format=csv" \
  -o july-https.csv

# Range summary as a human-readable block
curl -H "X-API-Key: $WEBHOOK_KEY" \
  "https://api.cpmp-site.org/webhook/reports/summary?format=text"

# One asset, to investigate a gap
curl -H "X-API-Key: $WEBHOOK_KEY" \
  "https://api.cpmp-site.org/webhook/reports/daily?asset=SOL"

# Start a refresh of today's numbers — returns immediately
curl -X POST -H "X-API-Key: $WEBHOOK_KEY" \
  "https://api.cpmp-site.org/webhook/reports/refresh"
# {"status":"started","message":"Delivery report refresh started"}

# Poll for the result (repeat every second or two until status is "done")
curl -H "X-API-Key: $WEBHOOK_KEY" \
  "https://api.cpmp-site.org/webhook/reports/refresh/status"
# {"status":"done","rows_updated":3,"refreshed_at":"2026-08-21T18:04:11Z"}

Both GET endpoints return the standard 12-field Unified Messaging Envelope with your payload in data, the same shape as every other endpoint in this infrastructure. Check status_code, throw on error, read data — and never write a fallback for a missing field, because there are none. Every field is always present. See the envelope reference for the full contract.

On refresh: it is asynchronous — the POST starts the recompute and returns 202 immediately with {"status":"started"}, it does not wait for the rollup to finish. Poll GET /webhook/reports/refresh/status (returns running, done with rows_updated, error, or not_found if nothing has been started) to learn when it's ready, then re-fetch /webhook/reports/daily or /summary. Starting a new refresh is limited to once per 60 seconds per subscription and recomputes a trailing few days. You rarely need it — the rollup refreshes itself every 15 minutes (section 11). It exists for the case where you are actively investigating something and do not want to wait for the next cycle. A POST that returns 429 or a status poll that reads already_running both mean the same thing: a refresh is already in flight or just finished, and your report is already current to within a few minutes.

9.3 Complete Field Reference

Daily rows

FieldMeaning
dayEastern calendar date
delivery_methodhttps or udp
push_eventsPushes that day on this transport
successful_events / failed_eventsDelivered and failed push counts
delivery_rate_pctSuccessful over total, as a percentage
asset_rowsTotal asset rows — raw delivered volume
distinct_assetsLargest single push that day, in distinct assets
configured_selected_assetsYour configured count as it stands now
typical_assets_per_pushMode of assets per push — the day's usual shape
avg_assets_per_pushAverage assets delivered per push. Coverage numerator.
avg_configured_per_pushRecorded configured count, push-weighted. 0 when unavailable.
coverage_denominatorThe exact value coverage was divided by. Never re-derive this yourself.
coverage_basisProvenance of that denominator — see 6.3
asset_coverage_pctCoverage percentage
min_coverage_pctWorst single push that day
pushes_at_full_coveragePushes that delivered every configured asset
avg_latency_ms / p95_latency_msAverage and 95th percentile latency

Range summary

FieldMeaning
total_push_eventsAll pushes across the range and both transports
total_successful / total_failedRange delivery totals
delivery_rate_pctRange-wide delivery rate
total_asset_rowsTotal asset rows delivered
avg_assets_per_pushPush-weighted average across the range
configured_max_assetsLargest denominator resolved in the range
asset_coverage_pctPush-weighted average of each day's own coverage
min_coverage_pctWorst push across measured days
pushes_at_full_coverageComplete pushes across measured days
pushes_coverage_measuredThe denominator for the two fields above — how many pushes they cover
avg_latency_msPush-weighted average latency
by_delivery_methodPer-transport breakdown. Check this first when a rate looks off.
Why the range coverage is a weighted average of daily figures rather than one big division. A range can span tier changes and asset-selection changes, so no single denominator is correct for all of it. Each day is measured against its own denominator, then combined weighted by that day's push count — so a busy day counts for more than a quiet one, and a day when you ran 30 assets is not measured against a week when you ran 9. It is slightly more work to compute and it is the only version that survives a plan change mid-range.

10. Your Reports in Your Language

Everything you read about your webhook delivery is available in 12 languages, driven by a single setting in your Account Dashboard. Not machine-translated on the fly in a browser widget — deliberately translated, stored, and served.

SurfaceIn your language?Detail
Account Dashboard, including the Delivery Reports and Delivery Log panelsYesEvery label, heading, column and control. Right-to-left layout for Arabic and Urdu.
Webhook emails — subscription confirmation, plan changes, asset auto-adjustmentYesComposed in your language, with per-string fallback so a partial gap can never produce an unreadable email.
This documentation, and the whole Document LibraryYesTranslated by our own engine, which protects code and diagrams from translation.
The price payload we push to your endpointDeliberately notFixed English field names, forever. This is a feature — see below.

The 12 languages are English, Spanish, Portuguese, French, German, Russian, Hindi, Urdu, Italian, Arabic, Japanese, and Chinese (Simplified).

10.1 The Boundary — Human Language and Machine Language

That last table row is the important one, and it is a principle rather than a gap.

We translate the understanding. We never translate the execution.

Your delivery payload has fixed English field names — asset, price, source, updated_at — and they will never change based on a language preference. Think about what the alternative would mean: a customer switching their dashboard to Japanese and their parser breaking in production because price silently became 価格. A language setting is a preference about reading. It must never be a breaking change to an integration.

This is the same discipline we apply to our documentation, which is why you can read a translated page in Urdu and copy-paste a code block from it that works on the first try — code blocks, commands, identifiers and diagrams are all protected from translation. Prose is for humans; identifiers are for machines. Confusing the two is how translated technical documentation becomes useless.

The payload does carry a language field. Every push includes it, set from your API language preference. It is an honest declaration of your preference rather than a description of the payload's contents — because a successful price push contains no human-readable prose to translate. It is asset symbols, numbers, exchange names and timestamps. The field exists because every response this infrastructure produces carries the same 12 envelope fields without exception, and consistency is worth more than omitting a field on the one payload where it has little to say.
Diagram 10.1 — Where Your Language Setting Reaches
flowchart TB
    SET["Account Dashboard
Profile settings"] SET --> PL["Correspondence language
preferred_lang"] SET --> AL["API language
api_lang"] PL --> DASH["Dashboard UI
Delivery Reports panel
12 languages · RTL aware"] PL --> MAIL["Webhook emails
confirmation · plan change
asset auto-adjustment"] PL --> DOCS["Document Library
this page, 12 languages"] AL --> ENV["Payload language field
declares your preference"] PAY["Price payload
asset · price · source · updated_at"] PAY --> FIXED["Field names fixed in English
never change with language"] ENV -.->|"travels with"| PAY style DASH fill:#064e3b,stroke:#10b981,color:#d1fae5 style MAIL fill:#064e3b,stroke:#10b981,color:#d1fae5 style DOCS fill:#064e3b,stroke:#10b981,color:#d1fae5 style FIXED fill:#1e3a5f,stroke:#60a5fa,color:#dbeafe style PAY fill:#334155,stroke:#64748b,color:#e2e8f0

10.2 One Setting, Two Choices

Your profile offers two language choices, and the distinction is worth thirty seconds of your time because they do genuinely different jobs.

SettingGoverns
Preferred languageEverything written to you — your dashboard, your emails, your invoices and checkout pages. If you only set one, set this one.
API response languageThe language declared on API responses, and the language of API message text where it applies.
Emoji status indicatorsNot a language, but it lives beside them: our status lines use indicators like ✅ by default. Switch to plain text if your log pipeline prefers it.

Both are saved from the same panel in one action, and both take effect immediately — your dashboard re-renders in the new language on the next load, and your emails follow from the next one sent.

Why this matters more than it may seem. This infrastructure is built and funded by Cross Power Ministries of Pakistan, and its revenue funds freedom from brick kiln debt bondage in Pakistan. Urdu is not an afterthought on a list of supported locales — it is the language of the people this work exists to serve. When we built multilingual support, we built it properly and to the same standard in all 12 languages, including full right-to-left layout. A developer in Lahore reads the same complete documentation, in their own language and correct script, as a developer in London.

11. Freshness — When the Numbers Update

Your delivery log is written the instant a push happens. Your delivery reports read the rollup, which is refreshed on a schedule. So the practical question is: how stale can a report be?

Answer: about 15 minutes, worst case.

PathCadenceWindow covered
Scheduled refresh inside AuroraEvery 15 minutes, continuouslyTrailing 2 days
BeastReconciler nightly runOnce nightly, 1 AM EasternTrailing 3 days, every subscription
Refresh Now / POST /webhook/reports/refreshOn demand, 60-second cooldownToday

Three overlapping paths, on purpose. The 15-minute cycle keeps your numbers current without you doing anything. The nightly pass is wider and covers every subscription, so a day is recomputed with full context after it has completely finished — and because its window is deliberately wider than one day, a single missed run heals itself on the next one rather than leaving a permanent hole. Refresh Now exists for when you are actively investigating and 15 minutes feels long.

Every refresh is an idempotent recomputation from the raw log, not an incremental adjustment. Running one twice changes nothing, and a day recomputed a week later produces the same answer it did on the day. That property is what lets us have three overlapping schedules at all without them corrupting each other.

Reading last_refreshed. Each rollup row carries the timestamp of its last recomputation. If a number surprises you, check it — a figure from four minutes ago is describing a day still in progress, and today's row will naturally read differently at 9 AM than it will at midnight.

12. What This Costs You

Nothing. There is no reporting tier, no per-query fee, no storage charge, no export limit, and no add-on to buy.

We want to be direct about why, because the reasoning is not charity and it is not a loss leader.

You are paying for delivery. Proof of delivery is part of delivery. A price feed you cannot audit is a price feed you have to take on faith, and asking you to pay extra for the evidence that we did the job you already paid for would be, frankly, a strange thing to sell. Metering it would also change our incentives in a direction we do not want: the moment reporting becomes a revenue line, there is quiet pressure to make the free view less useful. We would rather every Associate be able to check our work, all the time, at no cost, and hold us to it.

It also happens to be inexpensive to provide, and the engineering is the reason. The rollup means your report reads about 60 rows rather than scanning millions. Recording the configured count costs nothing because the value was already in memory. Storage is a table that prunes itself nightly. Good architecture is what makes generosity affordable — if this had been built as a naive query against the raw log, it would have been slow, expensive, and inevitably rationed.

What we believe you will not find elsewhere at this price

We have earned the right to say a few things plainly.

Where to go next. New to Webhook Push? Start with Associate Onboarding for subscribing, configuring your endpoint, and verifying delivery. Building against the API? See the API Reference and the Unified Messaging Envelope. Tracking Pull API usage instead? That is LRS Report Management.

And if a number in your reports ever looks wrong — especially if it looks impossible — please open a support ticket and tell us. Our assistant Rhema will pick it up immediately and route it. Two of the three defects described in this document were found by someone looking at a figure and saying "that cannot be right." That instinct is genuinely valuable to us, and we would rather hear it than not.