# 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 or read-only 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, in any case. An unknown key returns an empty page rather than an error.
- `direction`: `inbound` or `outbound`.
- `status`: `success` or `error`.
- `trace`: a `traceGuid`. Returns every entry for that document, including the 997 acknowledgment if one was sent. A trace with no entries returns an empty page.
- `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`.
- `compact`: `true` to return each entry as `id`, `createdAt`, `direction`, `status`, `partnerKey`, `traceGuid`, `resultId` and `errorMessage`, without the embedded `detail`. `errorMessage` is cut to 200 characters. Use it to scan or count entries; a poller that processes documents needs `detail`, so it leaves `compact` off.

### 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`. It is the same object described on the [Results and Artifacts](/resources/api-docs/results) page, so an error entry also carries `transformations`, showing how far the document got before it failed.

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.

## Resends and Redeliveries in the Feed

A resent outbound document adds its own success entry on the original `traceGuid`, with `detail.resend` set to `true`. The original entry stays as it was, and the new entry's `detail.artifacts` lists the same artifacts, so the delivered EDI downloads from either one. The new entry describes the delivery, so its `detail` carries `sftp` where the document went over SFTP and has no `transformations`. If you count one delivery per document, deduplicate success entries on `traceGuid` rather than skipping the ones marked `resend`: when the first delivery failed, the resend is the only success entry. A resend that fails adds an error entry, also marked `resend`.

A redelivered inbound document behaves the same way: its own success entry lands on the original `traceGuid`, marked `detail.resend: true`, and the original entry is not duplicated.

## 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`, or read the entry's embedded `detail` directly: its `transformations` array shows which processing steps ran before the failure.

For the node-by-node account of what happened, read the run's log entries with `GET /platform/logs?trace=...`. See the [Logs page](/resources/api-docs/logs).

### Recovering a Delivery Your Endpoint Failed

Tediware already retries a delivery automatically when the failure looks transient (a server error such as 500 or 502, 429, a timeout, or a refused or reset connection), for about 16 minutes; see [Retry Behavior](/resources/api-docs/webhooks#retry-behavior). For anything that stays failed, such as a 400 or a 500 that lasted through every retry, or for a receiver that lost the document after already answering 2xx, here is how to get it back.

For an inbound document, redeliver it: `POST /platform/edi_transactions/:id/redeliver` re-runs the same delivery on the same trace, with no re-parse and no new transaction. See [EDI Transactions](/resources/api-docs/edi-transactions).

You can also pull the document yourself from the feed, which is the only option for an outbound confirmation and a fine backstop even once you are using redeliver:

1. Poll `GET /platform/feed_entries?status=error`.
2. Take the `resultId` from the entry.
3. Fetch the result with `GET /platform/results/:id` and find its output artifact.
4. Download it with `GET /platform/artifacts/:id` and process it as you would have on delivery.

This works for a partner on webhooks as well as one on polling: the feed is populated either way. It is worth wiring up before you need it, because it is the recovery path for any outage on your side, not only for a bug in one handler.

Tediware has no way to know you handled the entry, so it stays in the feed as an error. Track what you have recovered on your side.

## 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.
