> This page is for Documentation.

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://fiftytwo-demo.ferndocs.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://fiftytwo-demo.ferndocs.com/_mcp/server.

# Consume transaction events

[Transaction POS Log](/api/transactions/overview) (TPLA) moves transaction data into the
event-driven space. Rather than long-polling a REST endpoint for new receipts,
FiftyTwo pushes events to you as they arrive.

## Why events

From the API's own documentation, the trade this makes:

* **No long polling.** Less network traffic, no "have you got anything yet" loop.
* **Real time.** You react as the transaction happens, not on your next poll.
* **Resilient.** If your consumer goes down, the data is ordered and waiting when
  it comes back.
* **Horizontally scalable.** Add consumers to share a high-volume workload,
  round-robin.
* **No status-code business logic.** An event arriving *is* the data.

The event payload is identical to the one the REST endpoints return, so you can
develop against REST and switch to events without reshaping your parser.

## Transport: Apache Pulsar

FiftyTwo uses [Apache Pulsar](https://pulsar.apache.org/). Topics are structured:

```bash
persistent://customer-name/chain-name/topic-name
```

Pulsar's multi-tenancy is why the topic path carries the customer and chain: your
data is namespaced from every other tenant's.

> **Note**
>
> You will need a Pulsar client for your language and the topic path plus
> credentials for your tenant. Ask your FiftyTwo consultant for those. They are
> provisioned per customer and are not in the OpenAPI spec.

## Consuming

```python
import json
import pulsar

client = pulsar.Client("pulsar+ssl://<broker>:6651", authentication=...)

consumer = client.subscribe(
    "persistent://your-customer/your-chain/transaction-pos-log",
    subscription_name="analytics-loader",
    # Shared lets you scale out: add consumers on the same subscription and
    # Pulsar round-robins between them.
    consumer_type=pulsar.ConsumerType.Shared,
)

while True:
    msg = consumer.receive()
    try:
        event = json.loads(msg.data())
        handle(event)                 # must be idempotent -- see below
        consumer.acknowledge(msg)
    except Exception:
        # Negative-ack so Pulsar redelivers rather than dropping the event.
        consumer.negative_acknowledge(msg)
```

## Design for redelivery

> **Warning**
>
> Pulsar gives you at-least-once delivery. Any event can arrive more than once:
> after a consumer restart, a negative ack, or a redelivery timeout. A handler
> that is not idempotent will double-count revenue.

Make the handler idempotent on the transaction's own identity:

```python
def handle(event):
    tx_id = event["transactionPosLogId"]
    if store.already_processed(tx_id):
        return                        # duplicate delivery; nothing to do
    store.record(tx_id, event)
```

Ordering is per-topic-partition, not global. If your logic depends on sequence,
key on the store's session and sequence numbers rather than assuming arrival
order.

## The REST endpoints

Three endpoints remain, and they are the right tool for backfill and
reconciliation rather than steady-state ingestion:

| Endpoint                                                           | Use                                                 |
| ------------------------------------------------------------------ | --------------------------------------------------- |
| `GET /v1/chain/{chain}/transaction-pos-log/{lastId}`               | Page through a chain's transactions from a known ID |
| `GET /v1/chain/{chain}/store/{store}/transaction-pos-log/{lastId}` | Same, scoped to a store                             |
| `GET /v1/chain/{chain}/store/{store}/session/{session}/end-of-day` | End-of-day report for a store session               |

The `lastTransactionPosLogId` cursor makes these useful for catching up after an
extended outage, or for verifying that your event pipeline has not silently
dropped anything.

#### Consume events for steady state

Events are the primary path. Everything current arrives here.

#### Reconcile nightly over REST

Walk the cursor for the day and compare counts against what you ingested. A
mismatch is the only reliable signal that a pipeline is dropping events.

#### Use end-of-day as the financial truth

For revenue reporting, reconcile against the EoD report rather than summing
events yourself. It is the store's own close-out.

## Migrating off the Receipt API

If you currently poll the [Receipt API](/api/transactions/receipt-api), this replaces
it. See [Migrating off the Receipt API](/api/transactions/receipt-api) for the
endpoint mapping.

## Related help-center topics

#### [Transaction types](/help/store-management/transaction-types)

What each transaction type means.

#### [Balance and bookkeeping](/help/concepts/till-balance-book)

How stores reconcile, and what your numbers should agree with.