---
title: "Batch market flow"
method: POST
path: "/api/v1/markets/flow/batch"
tags: ["Markets"]
---

# Batch market flow

`POST /api/v1/markets/flow/batch`

## Request

- Base URL: `https://api.0xinsider.com`
- URL: `https://api.0xinsider.com/api/v1/markets/flow/batch`
- Auth: one of:
  - HTTP bearer
  - OAuth 2
    - Authorization code: `https://0xinsider.com/oauth/authorize`
    - Token URL: `https://api.0xinsider.com/oauth/token` scopes: `read`
- TypeScript: <https://skmtc.dev/0xinsider/apis/0xinsider-api/docs/api/v1/markets/flow/batch/post.ts> (a function with typed inputs; its response is checked against the schema)

Returns each market's flow and top positions for 1-25 raw provider condition_id values. Results preserve request order, duplicate inputs return duplicate rows, and each item is charged one batch item unit before execution. Do not pass prefixed mkt_ IDs; use market.condition_id from search or explore.

## Headers

- `X-Query-Validation` 'strict'

## Request body

- object
  - `condition_ids` string[], required: Market condition IDs to resolve in input order.
  - `timeframe` '1h' | '4h' | '24h' | '7d': Lookback window used for flow and trade-count context.

## Response `200`

Ordered batch market-flow results

- object
  - `object` 'market_flow_batch', required
  - `data` BatchMarketFlowItem[], required
    - `index` integer, required: Zero-based request index. Duplicate inputs keep separate result rows.
    - `input` string, required
    - `status` 'ok' | 'error', required
    - `data` MarketFlow
      - `market` object, required
        - `id` string, required
        - `condition_id` string, required
        - `title` string, required
        - `slug` string, nullable, required
        - `category` string, nullable, required
        - `platform` string, nullable, required
      - `sharp_money` object, required: Outcome-aware flow from all tracked whale trades in the window, without a grade filter. BUY YES and SELL NO add net exposure; BUY NO and SELL YES subtract it. Gross buy/sell volumes count both outcomes. Top positions are separately graded. Direction uses unrounded net: negative is NO, otherwise YES; the zero tie-break is not conviction. Canonical; smart_money is a deprecated byte-identical alias.
        - `net_flow_usd` number, required
        - `direction` 'YES' | 'NO', required
        - `token_id` string, nullable, required: The Polymarket CLOB token id (ERC1155 asset id, decimal string) for the net-flow direction outcome; null when unavailable (e.g. unsynced markets).
        - `large_trade_count` integer: Canonical key since #16304 (Polymarket's noun is large trade); whale_trade_count is its deprecated spelling, emitted beside it with the same value.
        - `whale_trade_count` integer, required
        - `buy_volume_usd` number, required
        - `sell_volume_usd` number, required
        - `top_positions` object[], required
          - `id` string, required
          - `address` string, required
          - `username` string, nullable, required
          - `grade` string, nullable, required
          - `side` 'YES' | 'NO', required
          - `token_id` string, nullable, required: The Polymarket CLOB token id (ERC1155 asset id, decimal string) for this outcome; null when unavailable (e.g. unsynced markets).
          - `size_usd` number, required
        - `oldest_snapshot_as_of` string, date-time, nullable, required: Age of the oldest stored position snapshot behind top_positions: the minimum wallet sync clock over the positions this body publishes. The roster is a stored read rather than a live one, and graded wallets sit on different sync tiers, so this names how current the whole roster is; the response is no fresher than this timestamp. Null when the roster is empty or no published position carries a sync clock. It does not date net_flow_usd, the volumes or the trade counts, which come from large-trade event times.
      - `smart_money` object, required: Deprecated alias of sharp_money; byte-identical and retained for backward compatibility.
        - `net_flow_usd` number, required
        - `direction` 'YES' | 'NO', required
        - `token_id` string, nullable, required: The Polymarket CLOB token id (ERC1155 asset id, decimal string) for the net-flow direction outcome; null when unavailable (e.g. unsynced markets).
        - `large_trade_count` integer: Canonical key since #16304 (Polymarket's noun is large trade); whale_trade_count is its deprecated spelling, emitted beside it with the same value.
        - `whale_trade_count` integer, required
        - `buy_volume_usd` number, required
        - `sell_volume_usd` number, required
        - `top_positions` object[], required
          - `id` string, required
          - `address` string, required
          - `username` string, nullable, required
          - `grade` string, nullable, required
          - `side` 'YES' | 'NO', required
          - `token_id` string, nullable, required: The Polymarket CLOB token id (ERC1155 asset id, decimal string) for this outcome; null when unavailable (e.g. unsynced markets).
          - `size_usd` number, required
        - `oldest_snapshot_as_of` string, date-time, nullable, required: Age of the oldest stored position snapshot behind top_positions: the minimum wallet sync clock over the positions this body publishes. The roster is a stored read rather than a live one, and graded wallets sit on different sync tiers, so this names how current the whole roster is; the response is no fresher than this timestamp. Null when the roster is empty or no published position carries a sync clock. It does not date net_flow_usd, the volumes or the trade counts, which come from large-trade event times.
      - `timeframe` string, required
    - `error` ApiErrorBody
      - `code` 'bad_request' | 'invalid_api_key' | 'subscription_required' | 'forbidden' | 'insufficient_scope' | 'not_found' | 'account_locked' | 'rate_limited' | 'rate_limit_unavailable' | 'internal_error' | 'request_timeout', required: FROZEN: an existing value never changes meaning. request_timeout (408, #16146) was added the way insufficient_scope was: the handler did not answer inside the server's 30-second timeout. Retry-After and retry_at ride on it only for a safe method (GET, HEAD); a timed-out mutation may have completed, so check its state and reuse its Idempotency-Key.
      - `message` string, required
      - `doc_url` string
      - `param` string
      - `retry_at` string, date-time: The recommended next retry instant (RFC3339). Present on every retryable error (reason=pick_not_released, code=rate_limited including reason=monthly_quota_exceeded, code=rate_limit_unavailable, reason=read_model_warming) and omitted otherwise. Always in the future. For pick_not_released: before the 11:00 UTC operating-window start, before a selected pick's stored release, or after a skipped day, it names the automatic system's next boundary. While no candidate exists in the live window it normally names the persisted next automatic selector attempt. Every value is advisory under supported operator actions: manual publication, release-time override, or admin generation can make a pick available first. When the automatic schedule is absent/due or a pick is overdue it degrades to ~60s. Schedule one request and do not poll. Prefer Retry-After for the duration because it is immune to client clock skew.
      - `freshness` FreshnessFailure
        - `max_age_s` integer, required: The caller's requested whole-response freshness ceiling in seconds.
        - `actual_age_s` integer: Age in seconds of the oldest stored data_quality.as_of clock, when one is available.
        - `as_of` string, date-time: The oldest stored data-quality clock used to calculate actual_age_s, when one is available.
        - `data_quality_status` 'fresh' | 'partial' | 'unknown' | 'untracked' | 'unavailable', required: The trader body's whole-response data-quality status. Only fresh can satisfy max_age_s.
      - `reason` 'cursor_expired' | 'unknown_endpoint' | 'pick_not_released' | 'trader_not_tracked' | 'read_model_warming' | 'database_unavailable' | 'request_accounting_unavailable' | 'idempotency_in_progress' | 'webhook_delivery_in_progress' | 'webhook_secret_rotation_not_prepared' | 'webhook_secret_rotation_overlap_active' | 'sandbox_api_key' | 'api_key_in_query' | 'subscription_inactive' | 'monthly_quota_exceeded' | 'invalid_query' | 'unknown_query_parameter' | 'invalid_path' | 'invalid_body' | 'unsupported_media_type' | 'payload_too_large' | 'method_not_allowed' | 'ip_rate_limited' | 'ip_throttled' | 'export_expired' | 'freshness_ceiling_unsatisfied': ADDITIVE (#7209). The specific, actionable cause behind `code`, when there is one more specific than the code itself. `code` keeps its published values, so existing clients are unaffected; new clients branch on `reason`. Omitted when the code already says everything we know. pick_not_released: no Pick of the Day is published for the current product day; schedule one request against retry_at instead of polling. unknown_endpoint: the PATH is not a route on this API -- read GET /api/v1, do not retry. trader_not_tracked: the wallet is real and the URL is right, but the trader is outside the HOT/WARM sync tiers -- stop asking for this wallet. cursor_expired: pagination went stale mid-walk -- re-request the first page and continue. read_model_warming: the requested endpoint cannot serve its read model yet; exact causes are endpoint-specific and can include a cold or contended refresh or a dependency that prevented refresh. database_unavailable: the API's database or its connection pool is temporarily unreachable (a connection-class failure, not a query fault); code stays rate_limit_unavailable, nothing is rate-limited, retry after Retry-After / retry_at. idempotency_in_progress: retain the exact Idempotency-Key and request body, then retry shortly. webhook_delivery_in_progress: retry the URL or signing-secret configuration change after the destination's active request completes. request_accounting_unavailable: accounting capacity is unavailable before the handler executes; retry after Retry-After / retry_at. sandbox_api_key: the credential is a sandbox key (oxi_sk_test_) from POST /api/v1/agents/register, which only the sandbox server accepts -- call the sandbox base URL with it, or get a live key or OAuth access token; do not retry it here. api_key_in_query: the key was sent as a ?token= query parameter, which no route reads because URLs land in logs and history; the key itself was not checked -- resend it as Authorization: Bearer. subscription_inactive: the key is valid but the account's Pro subscription has lapsed (402 subscription_required); permanent until a person reactivates at https://0xinsider.com/billing, which the message names -- stop retrying on a schedule and surface the link. The key owner is emailed once per lapse. monthly_quota_exceeded: the account has used the requests Pro includes for the UTC calendar month (429 rate_limited); retry_at and Retry-After name the first of next month, the only retry that can succeed, and the message names https://0xinsider.com/developers, where pay as you go for requests over the quota is turned on. The X-Monthly-Quota-Limit, X-Monthly-Quota-Remaining and X-Monthly-Quota-Reset headers on every authenticated response say how close the account is. invalid_query, invalid_path, invalid_body (400 bad_request, #16146): a query parameter, a path segment or the JSON body did not parse or does not fit the route's schema, so no handler ran; param names the field when the parser named one (a query key, a path segment, a JSON path such as traders[0], or body); fix the request, never retry it as sent. unsupported_media_type (415 bad_request, param content-type): send the body with Content-Type: application/json. payload_too_large (413 bad_request, param body): the body is over 1048576 bytes. method_not_allowed (405 bad_request): the path is a route but not with this method; the Allow header names the methods it serves. ip_rate_limited (429 rate_limited, #16380): the per-address budget every caller behind one IP shares, counted before authentication, is spent; not the key's own window, and the RateLimit-* headers describe that bucket. ip_throttled (429 rate_limited): the address is in a cooldown after sustained over-limit traffic; Retry-After is minutes to days, and a request before it does not shorten the cooldown.
  - `meta` BatchResponseMeta, required
    - `request_id` string, required: Unique request ID (req_ prefix). The same value as the X-Request-Id response header, the request's usage accounting row and its log lines.
    - `cached` boolean, required
    - `total_items` integer, required
    - `successful_items` integer, required
    - `failed_items` integer, required
    - `request_cost` integer, required: Number of batch item units reserved before execution.
    - `rate_limit` BatchRateLimitMeta, required
      - `basis` 'batch_items_per_minute', required
      - `limit` integer, required
      - `remaining` integer, required
      - `reset` integer, required: Unix timestamp when the batch item window resets.

## Other responses

- `400`: Invalid request parameter
- `401`: Missing or invalid API key
- `402`: Active Pro subscription required. The key is valid but the account has no active Pro subscription; error.reason is subscription_inactive and error.message names the reactivation URL (https://0xinsider.com/billing). Permanent until a person reactivates: no Retry-After, never retry on a schedule.
- `403`: Account access denied
- `408`: The handler did not answer inside the server's 30-second timeout. error.code is request_timeout. On GET and HEAD the response carries Retry-After and error.retry_at; on a mutation it carries neither, because the request may have completed on the server: check its state before repeating it, and reuse its Idempotency-Key.
- `413`: The request body is over 1048576 bytes. error.code is bad_request, error.reason is payload_too_large and error.param is body.
- `415`: The body was not sent as Content-Type: application/json. error.code is bad_request, error.reason is unsupported_media_type and error.param is content-type.
- `423`: Account is locked
- `429`: Rate limit exceeded. Three independent budgets. (1) 100 requests/minute per user (sliding window), on every authenticated route. (2) On the BATCH routes only: 2500 batch item units/minute per user, reserved before any item is executed. A batch with N requested items costs N item units, including duplicate and invalid items. 2500 = 100 requests x 25 items per batch, which is the most item work a key can buy through the request limiter at all: a caller may spend their entire 100-request minute on full 25-item batches without the item budget being what stops them. The REQUEST budget is the effective ceiling, and batching is never the more expensive choice. The item budget can still deny at a sliding-window boundary (both counters carry the previous window forward with a floor, and the item counter runs 25x the request counter), so honor a 429 from either. Over-quota batches return 429 with Retry-After plus RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset before any item work is done. (3) The monthly quota: Pro includes 250,000 authenticated requests per UTC calendar month, whatever the billing cadence. Over 250,000: with pay as you go on, requests keep answering and the excess bills at USD 0.20 per 1,000 on a monthly invoice, up to 1,000,000 requests a month; without it, from October 1, 2026, the next request answers 429 rate_limited with error.reason monthly_quota_exceeded and a Retry-After to the month's reset, and from the same day a pay-as-you-go account answers the same past 1,000,000. A refused request is not counted. Every authenticated response carries X-Monthly-Quota-Limit, X-Monthly-Quota-Remaining, and X-Monthly-Quota-Reset (unix seconds, the first of next month). (4) The per-address budget: 1200 requests/minute per IP, shared by every caller behind one address and counted before authentication, on every route. A 429 from it carries error.reason ip_rate_limited and describes that bucket in RateLimit-*; a throttled address (sustained over-limit traffic) carries error.reason ip_throttled with a Retry-After of minutes to days, and a request before it does not shorten the cooldown. Every 429 is the standard error envelope with meta.request_id equal to X-Request-Id.
- `500`: Unexpected server error
- `503`: Redis-backed authenticated rate limiter unavailable; retry after the per-process outage cooldown

## Changes

- **2026-09-23** `2035b3316329`: 1 info
  - endpoint added

[Operation history](https://skmtc.dev/0xinsider/apis/0xinsider-api/changes/api/v1/markets/flow/batch/post.md)

---

[API](https://skmtc.dev/0xinsider/apis/0xinsider-api.md) - [All operations](https://skmtc.dev/0xinsider/apis/0xinsider-api/llms.txt) - [OpenAPI document](https://skmtc.dev/0xinsider/apis/0xinsider-api/revisions/2035b3316329?raw)
