Crossdeck Docs
Dashboard

Rail Ledger

Workbench 7 min read · Go to the bank, get the ledger

The Rail Ledger answers one question with no room for interpretation: for a given transaction, what does the payment rail itself say right now — and does it match what Crossdeck stored? For every transaction id Crossdeck has indexed, the tool queries Apple or Stripe directly and lays the rail’s authoritative status next to your stored record. Two columns of fact. You read the difference; the tool never declares one.

TL;DR

What it pulls

The Rail Ledger does not crawl the rail for everything it has ever seen — no rail offers that to an indie developer, and Apple offers no event firehose at all. Instead it works from the index Crossdeck already maintains: the complete, ordered set of transaction ids tied to your project. That set is the union of three sources.

SourceWhat it contributes
SubscriptionsEvery subscription Crossdeck mirrors, keyed by its original transaction id.
PurchasesEvery one-off purchase — consumable, non-consumable, non-renewing.
Audit logTransactions named in a rail notification that persisted no money record — e.g. a deferred or unconnected-project event. These show STORED — no record, which is a fact to read, not an error.
It shows Crossdeck-indexed ids, not Apple’s entire holdings.

A transaction that occurred but was never notified to Crossdeck is invisible to any tool — there is no endpoint to enumerate it. The Rail Ledger is honest about its scope: it reconstructs the ledger for the ids Crossdeck knows about, which for a correctly wired project is every transaction the rail has told it about.

Running a pull

Open Developers → Workbench → Rail Ledger. Three controls run a pull:

Each row carries a rail-verified timestamp so you know the RAIL column is live, not cached. A filter box narrows the table by transaction id, customer id, or status.

Reading a row

A row reads left to right as a parsed statement, with the two columns that matter — RAIL and STORED — at its center.

ColumnWhat it shows
TransactionThe rail’s original transaction id (Apple) or object id (Stripe), and the Crossdeck customer it resolved to. Both copy on click.
TypeThe headline. Subscription, one-off, consumable, or non-consumable — read directly from the rail.
ProductThe product identifier the rail names on the transaction.
AmountPrice and currency, when the rail returned them (see the note under What Apple returns).
Rail stateRAIL. The authoritative status the rail returned just now — e.g. ACTIVE, EXPIRED, REFUNDED.
StoredSTORED. The state Crossdeck holds for the same transaction, or no record.
Renews / expiresThe relevant date, plus the billing interval for subscriptions.
EnvProduction or sandbox. The pull queried the matching rail environment.

Every row also has a View raw control. Expand it to read the rail’s literal decoded payload for that transaction — Apple’s decoded signed transaction, Stripe’s object JSON — exactly as the rail returned it. The structured row is for legibility; the raw payload is for the moments you trust nothing and want the rail’s own words. Rail-specific fields that don’t fit the shared columns (Apple’s ownership type, Stripe’s dispute detail) live here, so the structured table stays normalized without hiding anything.

What Apple returns

Apple identifies a transaction by its originalTransactionId. The Rail Ledger calls the right App Store Server API endpoint for the transaction’s kind: for an auto-renewable subscription it reads the live renewal status; for a one-off it reads the transaction record directly. The decoded transaction carries the fields that fill the structured row.

Apple typeShown asMeaning
Auto-Renewable SubscriptionSubscriptionRenews until canceled; RAIL state is the live renewal status.
Non-ConsumableNon-consumableBought once, owned forever.
ConsumableConsumableBought and spent; can be bought again.
Non-Renewing SubscriptionNon-renewingA fixed-term entitlement with no auto-renew.

For a one-off, the RAIL state is read straight off the transaction: a present revocation date reads REFUNDED, a past expiry reads EXPIRED, otherwise PURCHASED. This is why a one-off shows its real status here rather than “no subscription status” — it has no renewal status to report, and the tool says what it actually is instead.

Price and currency are only present on newer Apple responses.

Apple began returning price and currency on the decoded transaction in recent versions of the App Store Server API; older transactions don’t carry them. When they’re absent, the Amount column is left blank — the Rail Ledger never invents a number the rail didn’t return.

What Stripe returns

Stripe’s objects are fully expressive, so the structured row fills in directly from a single retrieve. The Rail Ledger reads a subscription by its sub_… id and a one-off by its charge id, through your connected Stripe account.

Stripe objectShown asRAIL state from
SubscriptionSubscriptionThe subscription’s status (active, past_due, canceled, trialing…), with interval and current-period end.
ChargeOne-offThe charge’s status, or refunded when the charge has been refunded.

Amount, currency, and dates come straight from the object. Stripe-specific detail beyond the shared columns — dispute state, payment method, the full item list — is available in the row’s raw payload.

Read-only, no verdicts

The Rail Ledger inherits the Workbench guarantees without exception.