Rail 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
- It pulls every transaction Crossdeck has indexed — the union of your subscriptions, one-off purchases, and any transaction named only in the audit log — and queries the owning rail for each.
- Two columns: RAIL and STORED. RAIL is what Apple or Stripe literally returned just now; STORED is what Crossdeck holds. They usually agree; when they don’t, you’ve localized the issue to one record.
- Each row parses into a structured statement — type, product, amount, state, dates — and expands to the literal decoded payload when you want to read the rail’s words unedited.
- Type is the headline. Subscription, one-off, consumable, non-consumable — read straight from the rail, so “what is this transaction” is answered at a glance.
- It shows only what the rail returned. A field the rail didn’t send is left blank, never inferred or fabricated.
- Read-only, no verdicts, both rails (Apple and Stripe).
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.
| Source | What it contributes |
|---|---|
| Subscriptions | Every subscription Crossdeck mirrors, keyed by its original transaction id. |
| Purchases | Every one-off purchase — consumable, non-consumable, non-renewing. |
| Audit log | Transactions 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. |
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:
- Rail — Apple or Stripe. Each rail is queried through its own authoritative API.
- Slice — Recent (the most recent indexed transactions) or All indexed (page through everything).
- Pull rail ledger — runs the queries and stamps each row with the moment the rail answered.
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.
| Column | What it shows |
|---|---|
| Transaction | The rail’s original transaction id (Apple) or object id (Stripe), and the Crossdeck customer it resolved to. Both copy on click. |
| Type | The headline. Subscription, one-off, consumable, or non-consumable — read directly from the rail. |
| Product | The product identifier the rail names on the transaction. |
| Amount | Price and currency, when the rail returned them (see the note under What Apple returns). |
| Rail state | RAIL. The authoritative status the rail returned just now — e.g. ACTIVE, EXPIRED, REFUNDED. |
| Stored | STORED. The state Crossdeck holds for the same transaction, or no record. |
| Renews / expires | The relevant date, plus the billing interval for subscriptions. |
| Env | Production 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 type | Shown as | Meaning |
|---|---|---|
Auto-Renewable Subscription | Subscription | Renews until canceled; RAIL state is the live renewal status. |
Non-Consumable | Non-consumable | Bought once, owned forever. |
Consumable | Consumable | Bought and spent; can be bought again. |
Non-Renewing Subscription | Non-renewing | A 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.
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 object | Shown as | RAIL state from |
|---|---|---|
| Subscription | Subscription | The subscription’s status (active, past_due, canceled, trialing…), with interval and current-period end. |
| Charge | One-off | The 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.
- Read-only. It issues authenticated reads to the rails and reads from Crossdeck’s store. It never writes, grants, refunds, mutates, or caches a rail response back as truth. The guarantee is pinned by a test that scans the tool’s source for any write API.
- No verdicts. The tool shows RAIL next to STORED and stops there. It never flags a row “divergent” or “wrong” — that judgment is yours to make from two facts in front of you.
STORED — no recordandRAIL — no statusare neutral statements, not alarms. - Environment-scoped. A sandbox transaction is verified against Apple’s sandbox or Stripe’s test mode; production against live. The two never mix.
Related
- Workbench — the hub the Rail Ledger lives in, and the read-only principles it inherits.
- Revenue intelligence — how the stored states in the STORED column are derived into the money numbers everywhere else.
- Apple one-off purchases — how consumables, non-consumables, and non-renewing subscriptions become the purchase records this tool reads.
- Rail webhooks — how rail events reach Crossdeck and populate the index the Rail Ledger walks.
- Sandbox vs production — why every pull is environment-scoped.