# Pull mode feed specification

What your endpoint has to return when Pinlyx polls it. Use pull mode when
you cannot add outbound calls to the source system, for example a hosted store
with no webhook support.

- **Method:** `GET`
- **Polling interval:** 5 to 1440 minutes, set per source
- **Auth:** none, `Authorization: Bearer <token>`, or a custom header

---

## Response shape

```json
{
  "events": [
    {
      "externalId": "order-2026-000481",
      "type": "sale",
      "amount": 149.90,
      "currency": "USD",
      "occurredAt": "2026-08-31T14:22:05Z",
      "status": "completed",
      "customer": {
        "email": "buyer@example.com",
        "name": "Ada Lovelace",
        "country": "GB"
      },
      "fees": { "payment": 4.65 }
    }
  ],
  "nextCursor": "eyJpZCI6NDgxfQ",
  "hasMore": true
}
```

| Field | Required | Notes |
|---|---|---|
| `events` | yes | Array. An empty array is a valid response. |
| `events[].externalId` | yes | Stable and unique per event. This is the idempotency key. |
| `events[].amount` | yes | Number, not a string. Use a negative amount for a refund. |
| `events[].occurredAt` | yes | ISO 8601 with a timezone. Not a local timestamp. |
| `events[].type` | no | `sale` (default) or `expense`. |
| `events[].currency` | no | ISO 4217. Defaults to the source's currency. |
| `events[].status` | no | `completed`, `pending`, `refunded`, `cancelled`. |
| `events[].customer.email` | no | Without it, no contact matching happens. |
| `nextCursor` | no | Opaque string. Returned to you as `?cursor=` on the next call. |
| `hasMore` | no | `true` means poll again immediately with the cursor. |

## Pagination

Pinlyx calls your endpoint with no cursor on the first request:

```
GET /feed/revenue
GET /feed/revenue?cursor=eyJpZCI6NDgxfQ
```

Keep paging while `hasMore` is true, up to 50 pages per run. Return at most
500 events per page. The cursor is opaque to us: encode whatever you need.

## Ordering and windows

Return events **oldest first**. On each run Pinlyx asks for everything since
the last successfully ingested event, so your endpoint should accept the cursor
as the only state. If you cannot support cursors, return the last 24 hours on
every call and rely on `externalId` for deduplication - it is less efficient
but perfectly correct.

## Errors

| Status | What Pinlyx does |
|---|---|
| 200 | Ingests, advances the cursor |
| 429 | Backs off and retries on the next scheduled run |
| 5xx | Retries with exponential backoff, three attempts |
| 4xx (not 429) | Marks the run failed and surfaces the body in the sync log |

Return a JSON body with a `message` field on errors. It is shown verbatim in
the sync log, which is the difference between a five-minute fix and an hour of
guessing.

## Before you enable the worker

Use **Test connection** in the source detail screen. It fetches the first page
and previews the parsed events without ingesting anything. Check that amounts
are numbers, dates carry a timezone, and `externalId` is genuinely stable
across calls: an id that changes between polls creates a duplicate every run.
