# Polling

Polling is the alternative to webhooks for learning that a document is ready. Instead of Tediware pushing a notification to an endpoint you host, your system pulls processing outcomes from a feed on its own schedule. Use it when your platform cannot accept inbound HTTP requests, such as a firewalled or fetch-only system. When you can host an HTTPS endpoint, webhooks are recommended instead.

Polling and webhooks deliver the same outcomes. A feed entry carries only identifiers (`resultId`, `traceGuid`), never document content, so your system pulls the full data through the Results and Artifacts API exactly as it would after a webhook.

## When to Use Polling

A partner is set to one delivery method or the other on its **Configuration** card:

- **Webhooks**: Tediware builds the partner's inbound, outbound, and error webhooks and pushes to them. Recommended when your system can receive HTTPS requests.
- **Polling**: Tediware builds none of those webhooks. Your system pulls the partner's inbound documents, outbound confirmations, and errors from the feed.

The feed is populated for every partner regardless of delivery method, so a partner on webhooks can also be polled, for reconciliation or as a backstop. Changing a partner's delivery method rebuilds its inbound and outbound flows.

## The Feed Endpoint

```
GET /platform/feed_entries
Authorization: Key <your-key>
```

The feed is an append-only stream of every deliverable document and error across your partners, ordered oldest first. Authenticate with an API key created on the **API Keys** page. The feed returns only your organization's entries. Sandbox keys cannot read the feed; use a standard key.

### Query Parameters

All parameters are optional. Without any, you get the oldest unread page of the whole feed.

- `partner`: a partner key to filter by. An unknown key returns an empty page rather than an error.
- `direction`: `inbound` or `outbound`.
- `status`: `success` or `error`.
- `since`: an ISO 8601 timestamp. Returns entries created at or after it. An invalid timestamp returns `400`.
- `cursor`: an opaque high-water mark from a previous page's `nextCursor`. Advances past everything already returned.
- `limit`: page size, default `50`, capped at `100`.

### Response

```json
{
  "feedEntries": [
    {
      "id": "8f1d3c2a-...",
      "direction": "inbound",
      "status": "success",
      "createdAt": "2026-06-08T14:22:33.482190Z",
      "partnerKey": "ACME",
      "traceGuid": "5ab72145-4a4b-40f6-95de-e2579f163f79",
      "resultId": "4c2021f8-8310-4628-80f9-b578d4d68e11",
      "detail": { "...": "..." }
    }
  ],
  "pagination": {
    "hasMore": true,
    "nextCursor": "MjAyNi0wNi0wOFQxNDoyMjozMy40ODIxOTBafDhmMWQzYzJh"
  }
}
```

Each entry contains:

- `id`: the feed entry's own identifier.
- `direction`: `inbound` or `outbound`.
- `status`: `success` or `error`.
- `createdAt`: when the entry was written, ISO 8601 UTC with microseconds.
- `partnerKey`: the key of the partner this document belongs to.
- `traceGuid`: the trace identifier linking all results from the same processing run.
- `resultId`: the processing result. Use it with `GET /platform/results/:id` to retrieve the full result and its artifacts.
- `detail`: the result embedded inline, including the artifact ids and any error detail, so the entry is self-sufficient. Download document bytes with `GET /platform/artifacts/:id`.

The `pagination` block contains `hasMore` (whether more entries are waiting beyond this page) and `nextCursor` (the cursor to pass on your next request).

## Cursor Pagination

The feed is a forward-only stream you advance with a cursor:

1. Make the first request with no `cursor` to start from the beginning, or with `since` to start from a point in time.
2. Process the returned entries and save `pagination.nextCursor`.
3. Pass that value back as `cursor` on the next request to continue past what you already received.
4. While `pagination.hasMore` is `true`, request the next page immediately, until it is `false`.
5. When `hasMore` is `false`, wait your chosen interval, then poll again with the saved cursor.

Because the cursor is a high-water mark over `(createdAt, id)`, an advancing poller never re-scans entries it has already seen. Persist the latest `nextCursor` so a restart resumes where it left off.

## The Safety Lag

The newest entries are withheld from the feed for about 2 seconds before they become visible. This safety lag guarantees that an entry which commits slightly out of timestamp order can never be skipped by an advancing cursor, which makes polling lossless.

The practical consequence: a document you just processed may not appear for a moment. This is expected and is not a lost delivery; it will be on the next poll. Polling faster than every couple of seconds gains nothing, since anything newer than the lag window is withheld and those requests mostly return empty pages.

## Choosing a Polling Interval

Pick an idle interval that matches how quickly you need documents. Anything from a few seconds to several minutes is reasonable, and most integrations are comfortable polling every minute or two. End-to-end latency is roughly your interval plus the 2-second safety lag. Whenever a response carries `hasMore: true`, drain the backlog with back-to-back requests rather than waiting out the interval between pages.

## Handling Errors in the Feed

Errors are recorded per processing attempt, so a document that is retried can produce more than one error entry, each sharing the same `traceGuid`. If you want one error per document, deduplicate on `traceGuid`. Fetch the result behind an error entry with `GET /platform/results/:id` to read its `errorMessage`.

## Rate Limits

The feed draws on the same per-key read budget as the rest of the Platform read API: 240 requests per minute. That budget is shared with result and artifact downloads, so the usual way to exceed it is a burst of artifact downloads after draining a large page, not the polling itself. When you exceed it, the API responds with `429` and a `Retry-After` header; wait that many seconds before retrying. See the Errors page for the full rate-limit response.

## In-App Discovery

For a partner set to **Polling**, Tediware shows a poll card on the partner page (scoped to the partner) and on each flow page (scoped to the partner and that flow's direction). Each card has a ready-to-copy `curl` command with the partner key already filled in, plus a link to create an API key. It is the fastest way to get the exact call for a partner.

## Troubleshooting

- **The feed is empty**: confirm the partner has processed at least one document, that you are using a standard (not sandbox) API key, and that your `partner`, `direction`, or `status` filters are not excluding everything.
- **A just-processed document is missing**: this is the safety lag. Wait a couple of seconds and poll again.
- **The same error appears more than once**: errors are per attempt. Deduplicate on `traceGuid`.
- **You keep seeing entries you already processed**: you are not passing `nextCursor` back as `cursor`, or you are not persisting it across restarts.
- **`429` responses**: you are exceeding the shared 240 requests per minute read budget, most often from artifact-download bursts. Back off for the `Retry-After` interval and spread downloads out.
