# Custom HTTPS pull contract — oweflow-read-v1

This document describes the implemented Custom API adapter, not a promise that an arbitrary merchant API already supports it. The merchant supplies an HTTPS base URL and a read-only bearer token. Revient imports selected order records into its canonical ingestion service and records source snapshots. An order alone does not establish a dispute or an enforceable debt.

## Authentication and network boundary

Every Custom API request is a GET with:

```http
Authorization: Bearer <merchant-issued-read-only-token>
X-Oweflow-Contract: oweflow-read-v1
Accept: application/json
Accept-Encoding: identity
```

The token is encrypted on the server and is never returned in connection status or client props. The base URL may contain a path, for example `https://api.example.com/revient`; it cannot contain credentials, a fragment, a query string, or a non-443 port. All DNS answers must be public, permitted addresses. The socket uses the validated address. Private, local, reserved and mapped-address destinations are refused. Redirects and compressed responses are refused.

Each response has a 2 MiB byte limit and each outbound request has a 15-second deadline including DNS resolution. Return JSON, using `null` or omission for unavailable facts. Do not send payment card numbers, CVV, passwords or unrelated customer history. Order numbers, transaction references and tracking identifiers are preserved; they are not classified as cards merely because their digits satisfy a checksum.

## Endpoints

Paths below are appended to the saved base URL. IDs are URL-encoded.

| Endpoint | Required | Response |
|---|---|---|
| `/health` | Yes | `200` and an object with optional `name` |
| `/orders?limit=25&updatedAfter=<ISO>&updatedBefore=<ISO>&cursor=<opaque>` | Yes for sync | `{ "orders": [{ "id": "order-1" }], "nextCursor": null }` |
| `/orders/{id}` | Yes | One order object, described below |
| `/orders/{id}/refunds` | Optional | Array of `{ "amountMinor": 250 }`; an empty array confirms no refunds |
| `/orders/{id}/fulfilments` | Optional | Array of shipment objects, described below |
| `/disputes?status=LOST` | Optional | Array of up to 1,000 dispute records |
| `/orders/{id}/identity` | Optional, selected routing enrichment | `{ "name": "…", "email": "…", "address1": "…", "address2": null }` |

The order index requests 25 rows and refuses larger pages. `items` and `result` are accepted aliases for `orders`, but `orders` is recommended. IDs must be present, unique within the page and at most 500 characters. Return IDs as strings to preserve leading zeros and long references. `nextCursor` is an opaque string of at most 2,048 characters or `null`. Revient never follows a returned URL. Repeated cursors stop the run.

Apply both time bounds to the order's actual modification timestamp and keep the result order stable, preferably by `(updatedAt, id)`. A cursor must continue the same bounded result set; it must not reset or change the time window. Include boundary timestamps: Revient deliberately overlaps successful windows by five minutes and deduplicates source records and snapshots. The first run covers 30 days; subsequent runs start from the last completed window. A run has at most 1,000 pages. Older records can be supplied through the shared CSV intake.

An optional disputes response is a bounded list, not an independently paginated dispute feed. Matching disputes are attached to orders included in the current order window. Update an affected order's modification timestamp when its dispute facts change. An absent or unavailable dispute resource leaves dispute evidence unknown. This adapter does not infer chargebacks from refunded or cancelled orders.

## Order and evidence shapes

```json
{
  "id": "order-1042",
  "number": "#1042",
  "placedAt": "2026-09-01T12:00:00Z",
  "updatedAt": "2026-09-30T10:00:00Z",
  "status": "completed",
  "currency": "GBP",
  "totalMinor": 12500,
  "paymentMethod": "card",
  "paymentProvider": "provider-name",
  "transactionReference": "txn-1042",
  "paymentStatus": "captured",
  "customer": { "id": "customer-8", "name": "Example Customer", "email": "customer@example.com", "phone": null },
  "billing": { "name": "Example Customer", "address1": "Example address", "city": "London", "postcode": "SW1A 1AA", "country": "GB" },
  "shipping": { "name": "Example Customer", "address1": "Example address", "city": "London", "postcode": "SW1A 1AA", "country": "GB" },
  "billingMatchesShipping": true
}
```

Money fields ending in `Minor` are non-negative integer ISO-currency minor units, not decimal major-unit amounts. Currency must be the source currency. Do not invent zero totals or dates. Unavailable refund resources produce unknown refund totals; an actual empty refund array produces zero. A missing optional identity, payment or fulfilment resource remains unknown.

Each `/fulfilments` member may contain `createdAt`, `carrier`, `trackingNumber` and `status`. A tracking number or shipment does not establish delivery; this contract does not automatically verify a carrier scan, signature, photo, policy acceptance or communications.

Each `/disputes` member contains `id`, `orderId`, `reason`, `status`, `amountMinor`, `currency`, `openedAt` and optional `closedAt`. Supported status values are `OPEN`, `WON`, `LOST`; other values become `UNKNOWN`. Use actual source dispute IDs. Evidence assessment runs after canonical ingestion, with AI disabled by default for automatic sync. Findings do not trigger customer contact, payment operations or case recovery.

## Errors and durable processing

Use `401` or `403` when the token is invalid or lacks permission, `404` for an absent optional resource, `429` when throttled, and `5xx` for transient failures. A rejected required read never advances the sync cursor or last successful sync time. The worker retries the same saved window with exponential backoff and a default maximum of five attempts. The current worker does not interpret `Retry-After`; merchants should provision a suitable read-only request budget. Optional-resource failures leave those facts unknown and do not fabricate success.

One worker job processes one page. The cursor and next job commit together. A crash can repeat a page; ingestion deduplicates by organisation, store, source identity and snapshot content. Pause stops reads at order boundaries. Disconnect retires access and preserves previously imported records.

## Optional signed inbound notifications

In the selected store's **Inbound webhook** setup, save a separate random secret of 32–256 non-whitespace characters. Do not reuse the API bearer token. Send a notification to:

```text
POST https://<revient-host>/api/storefronts/<storeId>/webhook
```

The request body is limited to 256 KiB. A minimal envelope is:

```json
{"eventId":"merchant-event-1042","type":"order.updated","orderId":"order-1042"}
```

Set `X-Oweflow-Timestamp` to the Unix time in seconds, then set `X-Oweflow-Signature` to the lowercase hexadecimal HMAC-SHA256 of `timestamp + "." + exactRawBody`, keyed with the separate webhook secret. Millisecond timestamps are also accepted. The timestamp must be within five minutes of receipt. Preserve the exact signed bytes when sending JSON.

An authenticated notification queues a normal authorised incremental read. Posted order/case facts are never accepted as the evidence source. `id` or `eventId` deduplicates the notification; if both are absent, the body hash is used. Success returns `202 {"accepted":true}`. A paused store acknowledges without starting reads. Unknown/disconnected stores and bad signatures return `401`; oversized bodies return `413`; invalid JSON returns `400`.

This is inbound store notification setup. It is separate from Revient's existing outbound webhook subscriptions.

## Verify your endpoint

Before relying on this connection, connect a merchant-owned test endpoint and compare a known order, currency, date, refund state and actual dispute facts with the source system. Also check pagination, rejected credentials, unavailable optional resources, retry behaviour and any signed notification setup. A successful health request confirms only that endpoint, not the correctness or completeness of every source record.

See the [Custom API setup guide](/resources/integrations/custom-api) and [import guide](/resources/integrations/import) for the Revient steps.
