PatternsRadar
Sign in

API reference

API & MCP

The same account, the same plan limits, without a browser. 20 REST endpoints and 10 MCP tools over 3,687 NSE instruments with daily history to 20 Aug 2026.

Quickstart · run a scan
curl -s -X POST "https://api.patternsradar.com/v1/scan" \
-H "Authorization: Bearer $PATTERNSRADAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"source": "close > ema(21) > ema(50) and volume > 2x avg(volume, 20)",
"universe": 500,
"limit": 50
}'

Overview#

Plain JSON over HTTPS. Everything lives under /v1; the path only changes when compatibility breaks.

Base URL
https://api.patternsradar.com

Requests and responses are JSON; send Content-Type: application/json on anything with a body. Every response carries an X-Request-ID header — echoed if you supply your own — worth logging, since quoting it is the fastest way for support to find your request. GET /healthz answers unversioned with {"status": "ok"} for probes.

Casing varies by surface and each endpoint below states its own: market responses are snake_case, and the scan envelope is camelCase with snake_case rows.

Authentication#

An API key sent as a bearer token. Keys are created on the account page and prove the same account a browser session does.

Authorization header
Authorization: Bearer prdr_YOUR_KEY

Keys start with prdr_. The quickest check that one works is GET /v1/auth/me, which answers with the account, plan and limits the key is operating under.

Keys are created and revoked in the browser, on the account page. Signing in, managing keys and billing are deliberately browser-only and have no API: a leaked key cannot mint replacements for itself or touch your subscription. The screening endpoints work anonymously; a key simply applies your plan’s rate limit instead of the shared per-IP one.

A key is shown once, when it is created — only a hash is stored, so it cannot be recovered, only revoked. Revocation takes effect immediately. Keep keys in an environment variable or a secret store, never in code.

Rate limits#

Two separate buckets per identity: scans and hit-rate replays are metered apart from ordinary lookups, so exhausting one does not lock you out of the other.

PlanAPI req/minScans/minSaved scansAlert scansWebhooksAPI keys
free602052
pro300100100505
max600240500200yes10

Limits smooth over the minute, and short bursts above the rate are absorbed — a page load or a batch of quick calls is not punished. Requests count against your API key when one is sent, otherwise against your IP. Over the limit answers:

429 · rate limited
HTTP/1.1 429 Too Many Requests
Retry-After: 23
Content-Type: application/problem+json

{
"type": "https://patternsradar.com/docs/api/errors#rate_limited",
"title": "too many requests",
"status": 429,
"detail": "too many requests — slow down or upgrade your plan",
"instance": "/v1/scan",
"code": "rate_limited",
"request_id": "6d0b7f3a9c214e08"
}

Honour Retry-After — it says how many seconds to wait. The numbers for every plan are on the pricing page; GET /v1/auth/me reports the limits your own key is operating under.

Errors#

Every failure is an RFC 9457 problem document — application/problem+json — so one parser reads anything that is not a success.

An error response
{
"type": "https://patternsradar.com/docs/api/errors#not_found",
"title": "not found",
"status": 404,
"detail": "no such scan",
"instance": "/v1/scans/42",
"code": "not_found",
"request_id": "6d0b7f3a9c214e08"
}

type identifies the kind of failure and resolves to its entry in the error catalog; code repeats its final segment so you can switch on a short string. title is constant per type, detail says what went wrong this time, instance is the path that answered, and request_id echoes the X-Request-ID header — quote it when reporting a problem. Every failure is exactly those seven members — a 429 and a plan limit included — so one parser reads all of them with no special cases. A plan limit is marked by its code, which always begins limit_; a rate limit carries its wait in the Retry-After header. For the numbers behind either, read GET /v1/plans or GET /v1/auth/me.

StatusCodeMeaning
400validationThe request itself is malformed — a body that is not JSON, a missing field, an over-long query, an id that is not a number. detail names what was wrong.
401unauthorizedNo credential where one was needed: no session cookie, or no bearer key on a route that requires one.
401session_expiredA session cookie was sent but no longer matches a live session — it expired or was revoked. Sign in again.
401invalid_api_keyA bearer key was sent but does not resolve to an active key — mistyped or revoked. The response carries a WWW-Authenticate header with error="invalid_token".
401invalid_credentialsLogin refused. One message for an unknown address and a wrong password, deliberately, so the endpoint cannot be used to discover which addresses are registered.
403forbiddenAuthenticated, but this account may not do that. The generic case; the plan limits below are the specific ones.
403session_onlyThe browser-only surface — key management, billing, signing out everywhere — refuses API keys: a leaked key must not be able to mint replacements for itself or touch billing. Sign in with a browser session instead.
403email_unverifiedThe account exists and is signed in, but its email address has never been confirmed. Two things need a confirmed address — switching on an alert, because we would be mailing an unproven address, and publishing a scan, because it becomes a public page. Everything else on the account works. POST /v1/auth/email/resend sends another link. This is not a plan limit and no upgrade clears it.
400invalid_tokenA signed link — an email confirmation, a password reset — did not verify. Either it was altered in transit, it was truncated by a mail client that wrapped the URL, or it has already been used. Ask for a new one.
410expired_tokenThe link verified but is past its lifetime: one hour for a password reset, three days for an email confirmation. Distinct from invalid_token on purpose, so the page can offer to send another rather than implying the link was wrong.
403limit_saved_scansThe account already keeps as many scans as the plan allows. Existing scans stay readable, runnable and deletable.
403limit_alerts_unavailableAlerting is part of the paid plans.
403limit_alert_scansAs many scans are already alerting as the plan allows.
403limit_webhooksWebhook delivery is part of the Max plan.
403limit_api_keysThe account holds as many active keys as the plan allows. Revoke one to make room.
403limit_mcpThe plan does not include Model Context Protocol access. Every current plan does; a plan can be configured without it.
403limit_rest_apiThe plan does not include calling the API with a bearer key. Only key-authenticated requests are refused — the app itself keeps working, because it signs in with a session.
403limit_scans_per_dayThe account has run as many scans today as the plan allows. Distinct from rate_limited, which is a 429 meaning "slow down and try again in a moment": this is the day's budget, and the fix is a different plan or tomorrow, not a retry. Anonymous callers never see it — they have no plan and no daily allowance, only the per-minute limit on their address.
403limit_hitrate_per_dayThe account has run as many hit-rate replays today as the plan allows. Its own allowance because a replay costs several times what a scan does, so spending one budget does not spend the other.
403scan_taken_downA published scan an operator has withdrawn. Not a plan limit — no upgrade lifts it, so the client must not offer one — and the detail carries the reason, which is meant to be shown to the author.
400invalid_couponNo such code, or it does not apply to the plan being bought, or it has been fully redeemed, or this account has already used it. The detail says which.
410coupon_expiredThe code was real and its window has closed. Distinct from invalid_coupon because there is nothing the customer can do about it.
409plan_archivedThe plan has been withdrawn from sale. Accounts already on it keep it and keep their limits; it simply takes no new subscribers.
404not_foundNo such route, symbol, scan, alert or key. Ownership is not disclosed: something that exists but belongs to somebody else answers the same way.
409conflictThe request collides with current state — a taken email, a duplicate scan name, a subscription already on that plan, nothing active to cancel.
429rate_limitedOver the per-minute allowance. The body is the plain problem document; the wait is in the Retry-After header, and the numbers per plan are in the rate-limits section.
500internalSomething went wrong on our side. The response says no more than that; quote the request_id when reporting it, and the request may be retried.
502upstreamA provider the request depends on did not answer usefully. Safe to retry; if it persists, wait a little and try again.
503billing_unconfiguredSubscription routes are present but cannot act on this deployment.
One deliberate exception: a Sift query the compiler refuses is not an HTTP error. POST /v1/scan and POST /v1/hitrate answer 200 with ok: false, the message, a hint and the span to underline — a syntax mistake is a result, not a failed request. Any non-200 from those routes is a problem document like everywhere else.
Two changes to migrate, if you wrote against an older version. Before August 2026 errors were a flat {"error": "…"} envelope: the message now lives in detail, so clients reading error should switch. Since then the body has become the same seven members whatever the status — limit, plan and upgrade are gone from the plan limits, and retry_after from the 429. Switch on code instead: anything beginning limit_ is a refusal a better plan lifts. The Retry-After header is unchanged and has always been sent, as are all success responses.

MCP server#

The Model Context Protocol endpoint, for Claude Code, Codex, Cursor and anything else that speaks MCP. An agent that connects can run scans, look up symbols, manage saved scans — and read the whole Sift language reference, so it writes correct queries without being taught.

MCP endpoint
https://api.patternsradar.com/mcp

Streamable HTTP, stateless, on the same host as the REST API. Authentication is an API key as a bearer header — required here, unlike REST: without one the endpoint answers 401 telling you what to send.

Connect a client

Terminal
claude mcp add --transport http patternsradar \
https://api.patternsradar.com/mcp \
--header "Authorization: Bearer prdr_YOUR_KEY"
Tool errors are results, not protocol errors: a refused query or an unknown symbol comes back as {ok: false, error, hint} so the model can read the message, fix the query and try again. The language reference is also exposed as an MCP resource, sift://reference (JSON), for clients that let users attach resources directly.

MCP tools#

Ten tools. Saved-scan tools operate on the account the key belongs to, under the same plan quotas as the HTTP routes. Expand one for its parameters and return shape.

run_scanRun a Sift screener query against NSE equities and return the matches. Call sift_reference first if unsure of the syntax.

Run a Sift screener query against NSE equities and return the matches. Call sift_reference first if unsure of the syntax.

Parameters

sourcestringrequired

The Sift query to run.

universenumberdefault 500

Liquidity universe: 100, 500 or 2000.

as_ofstringoptional

Run as of this session (YYYY-MM-DD); defaults to the latest.

limitnumberdefault 100, max 500

Maximum rows to return.

Returns: {ok, as_of, count, matches, truncated} — each match trimmed to symbol, name, close, change_pct, volume, turnover, rsi_14 and delivery_pct, far narrower than the HTTP rows, to spare the model's context.

hitrateReplay a Sift query across past sessions and report what its matches did over user-chosen forward horizons: win rate, payoff, excess return vs an index, a monthly breakdown and an equity-curve approximation. A sketch, not a backtest: no costs or slippage, and everything it reports describes past matches, never a prediction.

Replay a Sift query across past sessions and report what its matches did over user-chosen forward horizons: win rate, payoff, excess return vs an index, a monthly breakdown and an equity-curve approximation. A sketch, not a backtest: no costs or slippage, and everything it reports describes past matches, never a prediction.

Parameters

sourcestringrequired

The Sift query to replay.

universenumberdefault 500

Liquidity universe: 100, 500 or 2000.

sessionsnumberdefault 250

How many past sessions to replay.

horizonsnumber[]default [1, 5, 20]

Forward windows in trading days, each 1–60, at most 4.

entryModestringdefault "close"

close (signal bar) or nextOpen (next session's open).

benchmarkstringdefault "NIFTY 50"

A NIFTY index name for excess returns, or none.

detailHorizonnumberdefault the largest horizon

Which horizon the monthly and equity-curve detail describes.

stopLossPctnumberdefault none

Simulate a stop-loss this many percent below entry; with a stop or target set, each trade exits at whichever of stop, target or horizon close comes first.

targetPctnumberdefault none

Simulate a profit target this many percent above entry.

fullbooleandefault false

Include the per-session equity-curve points, histogram bins and per-trade list. Off by default — they are chart and audit data, and large.

Returns: The same shape as POST /v1/hitrate: sessions, totals, per-horizon buckets with payoff, exit-breakdown and benchmark stats, the monthly breakdown, and drawdown numbers. Curve points, histogram bins and trades only with full.

search_symbolsFind NSE symbols by ticker or company name.

Find NSE symbols by ticker or company name.

Parameters

querystringrequired

Ticker or company name, whole or partial.

limitnumberdefault 10, max 50

Maximum results.

Returns: {results} — the same hits as GET /v1/search.

get_symbolIdentity and latest session for one NSE symbol: name, ISIN, listing dates, liquidity tier, last close and average delivery percentage.

Identity and latest session for one NSE symbol: name, ISIN, listing dates, liquidity tier, last close and average delivery percentage.

Parameters

symbolstringrequired

The NSE ticker, e.g. RELIANCE.

Returns: {ok, symbol, stats} — stats included best-effort; {ok: false, error} for an unknown ticker.

get_barsRecent OHLCV bars for one symbol, most recent last.

Recent OHLCV bars for one symbol, most recent last.

Parameters

symbolstringrequired

The NSE ticker, e.g. RELIANCE.

timeframestringdefault "1d"

1d, 1w or 1mo.

limitnumberdefault 120, max 1000

How many most-recent bars to return.

Returns: {ok, bars, count}.

market_metaWhat the market database covers: first and last session, row counts, and how many symbols. Call this to find out how fresh the data is.

What the market database covers: first and last session, row counts, and how many symbols. Call this to find out how fresh the data is.

No parameters.

Returns: The same shape as GET /v1/meta.

list_saved_scansList the scans saved to the authenticated account.

List the scans saved to the authenticated account.

No parameters.

Returns: {scans} — id, name, source and universe for each.

save_scanSave a Sift query to the account. Saving under a name that already exists replaces that scan rather than creating a second one.

Save a Sift query to the account. Saving under a name that already exists replaces that scan rather than creating a second one.

Parameters

namestringrequired

What to call the scan; saving over an existing name revises it.

sourcestringrequired

The Sift query.

universenumberdefault 500

Liquidity universe: 100, 500 or 2000.

Returns: {ok, scan} — the same plan quota as the HTTP route applies; an agent with a key is not a way around a limit.

delete_scanDelete a saved scan by name.

Delete a saved scan by name.

Parameters

namestringrequired

The name of the saved scan to delete, matched case-insensitively.

Returns: {ok}, or {ok: false, error} when no scan has that name.

sift_referenceThe Sift query language: every field, indicator, window, operator and pattern that can appear in a scan. Read this before writing a query.

The Sift query language: every field, indicator, window, operator and pattern that can appear in a scan. Read this before writing a query.

No parameters.

Returns: The full machine-readable reference: syntax, examples, fields, indicators, window functions, operators, patterns, universes and notes — generated from the same catalog the compiler reads.

Market data#

Symbols, bars, stats and search. All of it is public — these answer without credentials, straight from the market database.

Database coverage#

GET/v1/metano auth

What the market database covers: first and last session, row counts, and how many symbols. Call it to find out how fresh the data is.

Response fields

first_datestring

The earliest session with bar data, YYYY-MM-DD.

last_datestring

The most recent session loaded. Scans, stats and alerts answer as of this day unless asked otherwise — compare it with today to judge freshness.

bar_rowsnumber

OHLCV rows in the database, across all symbols and history.

metric_rowsnumber

Rows of derived per-session metrics — returns, RSI, moving averages and the rest of the column vocabulary.

symbol_countnumber

Instruments in the database.

metric_yearsnumber

How many years back the derived metrics reach. Bars go back further — to first_date — but scans need metrics, so this is the scannable window.

Errors

Request
curl -s "https://api.patternsradar.com/v1/meta"
Response200
{
"first_date": "1995-01-02",
"last_date": "2026-08-14",
"bar_rows": 14162010,
"metric_rows": 5834290,
"symbol_count": 2412,
"metric_years": 10
}
Error response429 · rate_limited
{
"type": "https://patternsradar.com/docs/api/errors#rate_limited",
"title": "too many requests",
"status": 429,
"detail": "too many requests — slow down or upgrade your plan",
"instance": "/v1/meta",
"code": "rate_limited",
"request_id": "6d0b7f3a9c214e08"
}

All symbols#

GET/v1/symbolsno auth

Every ticker in the database, as a flat list. Meant for sitemaps and bulk jobs; it changes only when the data is rebuilt.

Response fields

symbolsstring[]

Every ticker in the database, sorted. Each is valid in the :symbol routes.

Errors

Request
curl -s "https://api.patternsradar.com/v1/symbols"
Response200
{
"symbols": ["20MICRONS", "21STCENMGM", "360ONE", "3IINFOLTD", "3MINDIA"]
}
Error response429 · rate_limited
{
"type": "https://patternsradar.com/docs/api/errors#rate_limited",
"title": "too many requests",
"status": 429,
"detail": "too many requests — slow down or upgrade your plan",
"instance": "/v1/symbols",
"code": "rate_limited",
"request_id": "6d0b7f3a9c214e08"
}

Symbol identity#

GET/v1/symbols/:symbolno auth

One symbol's identity: name, ISIN, listing dates, bar count, liquidity rank and tier.

Path parameters

symbolstringrequired

The NSE ticker.

Response fields

symbolstring

The canonical NSE ticker.

display_symbolstring

The ticker as displayed. Usually identical to symbol.

namestring

The full company or instrument name.

isinstringnullable

The instrument's ISIN.

kindstring

The instrument class — equity, index, etf.

seriesstringnullable

The NSE trading series code — EQ, BE — not a data series.

first_datestring

The first session with a bar on record, YYYY-MM-DD.

last_datestring

The most recent session with a bar.

bar_countnumber

Daily bars on record — the depth of history behind the metrics.

last_closenumbernullable

Close of the most recent session.

avg_turnover_20dnumbernullable

Mean daily traded value over the last 20 sessions, in rupees — the liquidity measure behind liquidity_rank and universe_tier.

liquidity_ranknumbernullable

Position when all equities are ordered by avg_turnover_20d1 is the most traded name on the exchange.

universe_tiernumber

The smallest liquidity universe the symbol belongs to: 100 means it is among the 100 most liquid equities by 20-day turnover, 500 within the top 500.

Errors

Request
curl -s "https://api.patternsradar.com/v1/symbols/RELIANCE"
Response200
{
"symbol": "RELIANCE",
"display_symbol": "RELIANCE",
"name": "Reliance Industries Limited",
"isin": "INE002A01018",
"kind": "equity",
"series": "EQ",
"first_date": "1995-01-02",
"last_date": "2026-08-14",
"bar_count": 7852,
"last_close": 2931.4,
"avg_turnover_20d": 16480200000,
"liquidity_rank": 3,
"universe_tier": 100
}
Error response404 · not_found
{
"type": "https://patternsradar.com/docs/api/errors#not_found",
"title": "not found",
"status": 404,
"detail": "not found",
"instance": "/v1/symbols/RELIANCE",
"code": "not_found",
"request_id": "6d0b7f3a9c214e08"
}

Symbol stats#

GET/v1/symbols/:symbol/statsno auth

The latest session's derived numbers for one symbol — returns, 52-week range, RSI, moving averages, delivery — plus an average delivery percentage over a chosen window.

Path parameters

symbolstringrequired

The NSE ticker.

Query parameters

delivery_sessionsnumberdefault 20, clamped 1..5000

Sessions to average delivery_pct over.

Response fields

statsobject

The latest session's derived numbers. Any of them can be null when the symbol's history is too short to compute it.

stats.dstring

The session the numbers describe, YYYY-MM-DD.

stats.closenumbernullable

The session's closing price.

stats.ret_1dnumbernullable

Percent change in close over the last trading day.

stats.ret_1wnumbernullable

Percent change in close over the trailing week.

stats.ret_1mnumbernullable

Percent change in close over the trailing month.

stats.ret_3mnumbernullable

Percent change in close over the trailing three months.

stats.ret_6mnumbernullable

Percent change in close over the trailing six months.

stats.ret_1ynumbernullable

Percent change in close over the trailing year.

stats.high_52wnumbernullable

Highest high of the last 52 weeks.

stats.low_52wnumbernullable

Lowest low of the last 52 weeks.

stats.pct_from_52w_highnumbernullable

Distance from the 52-week high, percent — 0 means at the high, negative below it.

stats.pct_from_52w_lownumbernullable

Distance above the 52-week low, percent.

stats.rsi_14numbernullable

14-session relative strength index, 0–100.

stats.atr_14numbernullable

14-session average true range, in price units.

stats.sma_20numbernullable

20-session simple moving average of close.

stats.sma_50numbernullable

50-session simple moving average of close.

stats.sma_200numbernullable

200-session simple moving average of close.

stats.delivery_pctnumbernullable

Percent of the session's traded shares actually taken into delivery rather than squared off intraday — from NSE delivery data.

stats.rel_volumenumbernullable

The session's volume as a multiple of its recent daily average — 2.0 is twice normal.

stats.turnovernumbernullable

Traded value for the session, in rupees.

stats.volumenumbernullable

Shares traded in the session.

avg_deliverynumbernullable

Mean delivery_pct over the delivery_sessions window requested — a steadier read of the stock's delivery character than any single session.

Errors

Request
curl -s "https://api.patternsradar.com/v1/symbols/RELIANCE/stats?delivery_sessions=20"
Response200
{
"stats": {
"d": "2026-08-14",
"close": 2931.4,
"ret_1d": 1.02,
"ret_1w": 2.4,
"ret_1m": 5.9,
"ret_3m": 9.1,
"ret_6m": 12.7,
"ret_1y": 18.3,
"high_52w": 3024.9,
"low_52w": 2221.05,
"pct_from_52w_high": -3.1,
"pct_from_52w_low": 32.0,
"rsi_14": 61.7,
"atr_14": 46.2,
"sma_20": 2876.3,
"sma_50": 2811.9,
"sma_200": 2694.5,
"delivery_pct": 58.2,
"rel_volume": 1.34,
"turnover": 18211430000,
"volume": 6214890
},
"avg_delivery": 54.6
}
Error response404 · not_found
{
"type": "https://patternsradar.com/docs/api/errors#not_found",
"title": "not found",
"status": 404,
"detail": "not found",
"instance": "/v1/symbols/RELIANCE/stats",
"code": "not_found",
"request_id": "6d0b7f3a9c214e08"
}

OHLCV bars#

GET/v1/symbols/:symbol/barsno auth

The full split-adjusted history for one symbol, oldest first — decades of it for an old listing.

Path parameters

symbolstringrequired

The NSE ticker.

Query parameters

timeframe"1d" | "1w" | "1mo"default "1d"

Bar size. daily, weekly and monthly are accepted as synonyms; anything else is a 400.

Response fields

barsobject[]

One candle per session, oldest first. Prices are split-adjusted, so a long history reads as one continuous series.

bars[].dstring

The session date, YYYY-MM-DD. For weekly and monthly bars, the period's last trading day.

bars[].opennumber

Opening price of the period.

bars[].highnumber

Highest trade of the period.

bars[].lownumber

Lowest trade of the period.

bars[].closenumber

Closing price of the period.

bars[].volumenumber

Shares traded over the period.

Errors

Request
curl -s "https://api.patternsradar.com/v1/symbols/RELIANCE/bars?timeframe=1d"
Response200
{
"bars": [
{
"d": "2026-08-14",
"open": 2902.0,
"high": 2940.8,
"low": 2897.15,
"close": 2931.4,
"volume": 6214890
}
]
}
Error response404 · not_found
{
"type": "https://patternsradar.com/docs/api/errors#not_found",
"title": "not found",
"status": 404,
"detail": "not found",
"instance": "/v1/symbols/RELIANCE/bars",
"code": "not_found",
"request_id": "6d0b7f3a9c214e08"
}

Screening#

The heart of the API: compile and run Sift queries. Both endpoints answer anonymous callers — the public screener runs on them — but a key applies your plan's rate limit instead of the shared per-IP one.

Run a scan#

POST/v1/scankey optional

Compile a Sift query and run it against a session. The source is exactly what you would type into the editor; the reference for the language lives at /docs/sift.

Body (JSON)

sourcestringrequired

The Sift query. At most 8,000 characters.

universenumberdefault 500, clamped 100..9999

The most liquid N equities by 20-day turnover. 9999 means all equities.

asOfstringoptional

Run as of this session, YYYY-MM-DD. Clamped to the database's range; defaults to the latest session. Scanning a past day costs the same as today.

limitnumberdefault clamped 1..2000

Maximum rows returned.

orderBystringoptional

Result column to sort by, e.g. turnover or rsi_14. The query's own sort by clause wins if both are given.

orderDir"asc" | "desc"optional

Sort direction.

kindsstring[]optional

Instrument kinds to include, e.g. ["equity"].

Response fields

okboolean

true when the query compiled and ran. false means the compiler refused it — the response then carries error, hint and span instead of rows; see the note below.

rowsobject[]

The matches, one per symbol, in the requested order. Every row carries the symbol's identity, the session's OHLCV and the standard derived columns below — the same columns whatever the query asked, so a client can parse rows once.

rows[].symbolstring

The NSE ticker — feed it to the symbol endpoints for detail.

rows[].namestring

The company or instrument name.

rows[].kindstring

The instrument class — equity, index, etf.

rows[].dstring

The session the row describes — an RFC 3339 timestamp at midnight UTC, unlike the plain dates elsewhere. Matches the envelope's asOf.

rows[].opennumber

The session's opening price.

rows[].highnumber

The session's high.

rows[].lownumber

The session's low.

rows[].closenumber

The session's closing price.

rows[].volumenumber

Shares traded in the session.

rows[].turnovernumber

Traded value for the session, in rupees.

rows[].delivery_pctnumbernullable

Percent of the session's traded shares actually taken into delivery rather than squared off intraday — from NSE delivery data.

rows[].rel_volumenumbernullable

The session's volume as a multiple of its recent daily average — 2.0 is twice normal.

rows[].rsi_14numbernullable

14-session relative strength index, 0–100.

rows[].sma_50numbernullable

50-session simple moving average of close.

rows[].sma_200numbernullable

200-session simple moving average of close.

rows[].atr_14numbernullable

14-session average true range, in price units.

rows[].adx_14numbernullable

14-session average directional index — trend strength, 0–100.

rows[].high_52wnumbernullable

Highest high of the last 52 weeks.

rows[].low_52wnumbernullable

Lowest low of the last 52 weeks.

rows[].pct_from_52w_highnumbernullable

Distance from the 52-week high, percent — 0 means at the high, negative below it.

rows[].ret_1dnumbernullable

Percent change in close over the last trading day.

rows[].ret_1wnumbernullable

Percent change in close over the trailing week.

rows[].ret_1mnumbernullable

Percent change in close over the trailing month.

rows[].ret_3mnumbernullable

Percent change in close over the trailing three months.

rows[].ret_1ynumbernullable

Percent change in close over the trailing year.

rows[].universe_tiernumber

The smallest liquidity universe the symbol belongs to: 100 means it is among the 100 most liquid equities by 20-day turnover, 500 within the top 500.

countnumber

How many rows came back — rows.length, for convenience.

truncatedboolean

true when more symbols matched than limit allowed. Raise the limit, or sort with orderBy so the rows you care about make the cut.

asOfstring

The session the scan actually ran against, YYYY-MM-DD — the requested asOf clamped to a real trading day.

elapsedMsnumber

How long the scan took to execute, milliseconds.

fieldsstring[]

The columns the query referenced — useful for showing which data a saved scan depends on.

warningsstring[]

Non-fatal notes the compiler attached. The query still ran; empty in the common case.

normalizedstring

The query as the compiler understood it, canonically spelled — confirm shorthand like 2x parsed the way you meant.

Errors

A query the compiler refuses is answered 200 with ok: false — a syntax mistake is a result, not a failed request: {"ok": false, "error": "unknown field: closr", "hint": "did you mean close?", "span": {"start": 0, "end": 5}, "line": 1, "col": 1}. The span offsets are UTF-16 code units, ready for an editor. Only a malformed request (bad JSON, source too long) or a genuine service fault is an HTTP error, answered as application/problem+json like everywhere else.
The request envelope is camelCase (asOf, orderBy) while row fields are snake_case (ret_1d, delivery_pct). A row's d is an RFC 3339 timestamp at midnight UTC, unlike the plain YYYY-MM-DD dates elsewhere; the envelope's asOf stays a plain date.
Request
curl -s -X POST "https://api.patternsradar.com/v1/scan" \
-H "Authorization: Bearer $PATTERNSRADAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"source": "close > ema(21) > ema(50) and volume > 2x avg(volume, 20)",
"universe": 500,
"limit": 50
}'
Response200
{
"ok": true,
"rows": [
{
"symbol": "POLYCAB",
"name": "Polycab India Limited",
"kind": "equity",
"d": "2026-08-14T00:00:00Z",
"close": 7412.5,
"open": 6981.0,
"high": 7448.0,
"low": 6975.2,
"volume": 1092480,
"turnover": 8123400000,
"delivery_pct": 44.1,
"rel_volume": 3.2,
"rsi_14": 71.3,
"sma_50": 6702.8,
"sma_200": 6120.4,
"atr_14": 182.6,
"adx_14": 31.9,
"high_52w": 7448.0,
"low_52w": 4211.0,
"pct_from_52w_high": 0.0,
"ret_1d": 6.8,
"ret_1w": 9.2,
"ret_1m": 14.5,
"ret_3m": 22.1,
"ret_1y": 61.0,
"universe_tier": 500
}
],
"count": 1,
"truncated": false,
"asOf": "2026-08-14",
"elapsedMs": 41,
"fields": ["close", "ema_21", "ema_50", "volume"],
"warnings": [],
"normalized": "close > ema(21) > ema(50) and volume > 2 * avg(volume, 20)"
}
Error response400 · validation
{
"type": "https://patternsradar.com/docs/api/errors#validation",
"title": "your request is invalid",
"status": 400,
"detail": "request body must be JSON",
"instance": "/v1/scan",
"code": "validation",
"request_id": "6d0b7f3a9c214e08"
}

Hit rate#

POST/v1/hitratekey optional

Replay a query across past sessions and report what its matches did over configurable forward horizons: win rate, payoff, excess return against an index, a monthly breakdown and an equity-curve approximation. A sketch, not a backtest: no costs or slippage, universe membership measured as of today — everything here describes past matches, never a prediction.

Body (JSON)

sourcestringrequired

The Sift query to replay. At most 8,000 characters.

universenumberdefault 500, clamped 100..9999

Liquidity universe.

sessionsnumberdefault 250, clamped 10..7500 — the whole history

How many past sessions to replay.

horizonsnumber[]default [1, 5, 20]

Forward windows in trading days, each clamped 1..60, at most 4 distinct values. Deduplicated and sorted ascending.

entryModestringdefault "close"

Where the position is priced: close (the signal bar's own close) or nextOpen (the next session's open — the earliest fill a reader of an EOD scan could take). Both exit at the close of bar signal+h.

benchmarkstringdefault "NIFTY 50"

Index for excess-return measurement: NIFTY 50, NIFTY 100, NIFTY 200, NIFTY 500, NIFTY BANK, NIFTY MIDCAP 150, NIFTY SMALLCAP 250, or none to skip it.

detailHorizonnumberdefault the largest horizon

Which horizon the monthly, histogram and equityCurve detail describes. Must be one of horizons.

stopLossPctnumberdefault none

Simulate a stop-loss this many percent below entry (clamped 0.1..95). Setting this or targetPct switches the replay into a trade simulation: each signal walks its forward bars and exits at the stop, the target, or the horizon close — whichever comes first. When one bar's range covers both levels, the stop wins; that is the conservative reading, stated rather than hidden.

targetPctnumberdefault none

Simulate a profit target this many percent above entry (clamped 0.1..95). Fills at the target price, no slippage.

Response fields

okboolean

true when the query compiled and the replay ran; false is a compiler refusal, answered the same way as /v1/scan.

sessionsnumber

How many past sessions were replayed.

sessionsWithSignalsnumber

How many of those sessions produced at least one match — a scan that fires on 8 days out of 250 behaves very differently from one that fires every day.

fromstring

The first replayed session, YYYY-MM-DD.

tostring

The last replayed session.

totalSignalsnumber

Matches summed over every replayed session — each symbol-day the query fired counts once.

signalsPerSessionnumber

totalSignals / sessions — the scan's average selectivity.

bucketsobject[]

One bucket per forward horizon — 1, 5 and 20 trading days — each measuring what all the signals did over that many sessions after firing.

buckets[].horizonnumber

Trading days ahead the bucket measures.

buckets[].nnumber

Signals with enough forward history to measure. Smaller at longer horizons — the most recent signals have not lived 20 sessions yet.

buckets[].meanPctnumber

Mean close-to-close return over the horizon, percent.

buckets[].medianPctnumber

Median return, percent — compare with the mean to spot a few big winners carrying an otherwise flat scan.

buckets[].winRatePctnumber

Percent of signals whose return over the horizon was positive.

buckets[].bestPctnumber

The single best signal's return, percent.

buckets[].worstPctnumber

The single worst signal's return, percent.

buckets[].stdDevPct / p25Pct / p75Pctnumber

Population standard deviation and the interquartile band of the per-signal returns — the shape around the mean.

buckets[].avgWinPct / avgLossPct / payoffnumber

Mean of the positive returns, mean of the rest (kept signed), and their ratio avgWin / |avgLoss|. Read win rate and payoff as a pair: a 45% win rate is fine at a 2.5 payoff and fatal at 0.8. There is no expectancy field — with these definitions it equals meanPct.

buckets[].truncatednumber

Signals excluded from the horizon because the dataset ends inside their forward window.

buckets[].benchmarkobject

Present unless the request said none: the index's own mean return over the same windows (meanPct), the mean and median per-signal excess (meanExcessPct, medianExcessPct), and what share of signals beat it (beatBenchmarkPct).

elapsedMsnumber

How long the replay took, milliseconds.

universenumber

The liquidity universe the replay used, echoed back.

entryMode / horizons / benchmark / detailHorizon / stopLossPct / targetPctvaries

The resolved choices, echoed back — what the numbers were actually computed with, after defaults and clamps.

buckets[].exitsobject

Present only under a simulated stop/target: how the horizon's trades ended — stop, target and time counts, summing to n.

tradesobject[]

Every detail-horizon trade, oldest first: d (signal session), symbol, entryPrice, exitD and exitPrice (the session it closed on and the stop/target level or that session's close), retPct, exitBar (sessions held) and exitReason (stop, target or time). Capped at 5,000 rows; tradesTruncated says when the cap bit. The audit trail behind the summary.

monthlyobject[]

The detail horizon by calendar month: month, signals, winRatePct, meanPct and, when benchmarked, meanExcessPct. The regime view — a scan whose whole edge sits in one hot quarter shows it here.

histogramobject

Distribution of per-signal returns at the detail horizon. edges (one more than counts) span the middle 96% of the data; outliers land in underflow/overflow rather than stretching the axis.

equityCurveobject

An index starting at 100 of equal-weighting every signal at the detail horizon, one point per session with signals, with maxDrawdownPct. Overlap-adjusted: each session's mean h-bar return is spread geometrically over h bars, so overlapping windows do not compound multiple times. method names the approximation — it is an approximation, not a track record. bench points build the same construction from the index's returns on the same sessions.

warningsstring[]

The caveats that bound these numbers: survivorship lean, thin samples, truncated horizons, missing benchmark days.

Errors

Compiler errors come back the same way as /v1/scan: 200 with ok: false, error, hint and a span. Any non-200 is a problem document.
Everything this endpoint returns describes what past matches did over a finite window. It is not a prediction, not a track record, and a universe measured as of today leans survivorship-positive in a way no disclaimer repairs — which is why the response carries its warnings with it.
Request
curl -s -X POST "https://api.patternsradar.com/v1/hitrate" \
-H "Authorization: Bearer $PATTERNSRADAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"source": "rsi(14) crossed above 30 within 3 bars",
"universe": 500,
"sessions": 250,
"horizons": [
1,
5,
20
],
"entryMode": "close",
"detailHorizon": 20
}'
Response200
{
"ok": true,
"sessions": 250,
"sessionsWithSignals": 212,
"from": "2025-08-08",
"to": "2026-08-14",
"totalSignals": 1834,
"signalsPerSession": 7.3,
"buckets": [
{
"horizon": 20,
"n": 1655,
"meanPct": 2.10,
"medianPct": 1.02,
"winRatePct": 56.3,
"bestPct": 64.5,
"worstPct": -31.0,
"stdDevPct": 8.9,
"p25Pct": -2.8,
"p75Pct": 6.4,
"avgWinPct": 7.1,
"avgLossPct": -4.3,
"payoff": 1.65,
"truncated": 179,
"benchmark": {
"meanPct": 1.32,
"meanExcessPct": 0.78,
"medianExcessPct": 0.22,
"beatBenchmarkPct": 52.8
}
}
],
"elapsedMs": 903,
"universe": 500,
"entryMode": "close",
"horizons": [1, 5, 20],
"benchmark": "NIFTY 50",
"detailHorizon": 20,
"monthly": [
{ "month": "2025-08", "signals": 141, "winRatePct": 58.2, "meanPct": 2.4, "meanExcessPct": 0.9 }
],
"histogram": {
"horizon": 20,
"edges": [-20, -15, -10, -5, 0, 5, 10, 15, 20],
"counts": [12, 96, 310, 402, 455, 244, 88, 48],
"underflow": 3,
"overflow": 17
},
"equityCurve": {
"horizon": 20,
"method": "overlap-adjusted-equal-weight",
"points": [
{ "d": "2025-08-08", "scan": 100.11, "bench": 100.06 }
],
"maxDrawdownPct": -9.4,
"benchMaxDrawdownPct": -6.1
},
"warnings": []
}
Error response400 · validation
{
"type": "https://patternsradar.com/docs/api/errors#validation",
"title": "your request is invalid",
"status": 400,
"detail": "request body must be JSON",
"instance": "/v1/hitrate",
"code": "validation",
"request_id": "6d0b7f3a9c214e08"
}

Saved scans#

The scans on your account — the same library the screener's sidebar shows, readable and writable with your key.

List saved scans#

GET/v1/scansAPI key

Every scan saved to the account.

Response fields

scansobject[]

Every scan on the account, one object each.

scans[].idnumber

The scan's id — what the :id routes, alerts and the delivery log key on. Stable for the scan's lifetime.

scans[].namestring

The name it was saved under, unique per account.

scans[].sourcestring

The Sift query, exactly as saved.

scans[].universenumber

The liquidity universe the scan runs against.

scans[].created_atstring

When the scan was first saved, RFC 3339 UTC.

scans[].updated_atstring

When it last changed — saving over the name or renaming moves it.

Errors

Request
curl -s "https://api.patternsradar.com/v1/scans" \
-H "Authorization: Bearer $PATTERNSRADAR_API_KEY"
Response200
{
"scans": [
{
"id": 42,
"name": "Delivery surge",
"source": "delivery_pct > 60 and volume > 2x avg(volume, 20)",
"universe": 500,
"created_at": "2026-07-02T09:14:11Z",
"updated_at": "2026-08-10T16:02:54Z"
}
]
}
Error response401 · unauthorized
{
"type": "https://patternsradar.com/docs/api/errors#unauthorized",
"title": "authentication required",
"status": 401,
"detail": "not signed in",
"instance": "/v1/scans",
"code": "unauthorized",
"request_id": "6d0b7f3a9c214e08"
}

Save a scan#

POST/v1/scansAPI key

Save a query under a name. Saving over an existing name revises that scan rather than creating a second one — an upsert, so revising never hits the quota.

Body (JSON)

namestringrequired

What to call it. At most 120 characters.

sourcestringrequired

The Sift query. At most 8,000 characters.

universenumberdefault 500

Liquidity universe the scan runs against.

Response fields

idnumber

The scan's id — what the :id routes, alerts and the delivery log key on. Stable for the scan's lifetime.

namestring

The name it was saved under, unique per account.

sourcestring

The Sift query, exactly as saved.

universenumber

The liquidity universe the scan runs against.

created_atstring

When the scan was first saved, RFC 3339 UTC.

updated_atstring

When it last changed — saving over the name or renaming moves it.

Errors

Request
curl -s -X POST "https://api.patternsradar.com/v1/scans" \
-H "Authorization: Bearer $PATTERNSRADAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Delivery surge",
"source": "delivery_pct > 60 and volume > 2x avg(volume, 20)",
"universe": 500
}'
Response200
{
"id": 42,
"name": "Delivery surge",
"source": "delivery_pct > 60 and volume > 2x avg(volume, 20)",
"universe": 500,
"created_at": "2026-07-02T09:14:11Z",
"updated_at": "2026-08-15T07:40:12Z"
}
Error response403 · limit_saved_scans
{
"type": "https://patternsradar.com/docs/api/errors#limit_saved_scans",
"title": "saved-scan limit reached",
"status": 403,
"detail": "You have reached the number of scans your plan can keep.",
"instance": "/v1/scans",
"code": "limit_saved_scans",
"request_id": "6d0b7f3a9c214e08"
}

Rename a scan#

PATCH/v1/scans/:idAPI key

Change a saved scan's name.

Path parameters

idnumberrequired

The scan's id, from the list.

Body (JSON)

namestringrequired

The new name. Non-empty, at most 120 characters.

Response fields

idnumber

The scan's id — what the :id routes, alerts and the delivery log key on. Stable for the scan's lifetime.

namestring

The name it was saved under, unique per account.

sourcestring

The Sift query, exactly as saved.

universenumber

The liquidity universe the scan runs against.

created_atstring

When the scan was first saved, RFC 3339 UTC.

updated_atstring

When it last changed — saving over the name or renaming moves it.

Errors

Request
curl -s -X PATCH "https://api.patternsradar.com/v1/scans/42" \
-H "Authorization: Bearer $PATTERNSRADAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Delivery surge v2"
}'
Response200
{
"id": 42,
"name": "Delivery surge v2",
"source": "delivery_pct > 60 and volume > 2x avg(volume, 20)",
"universe": 500,
"created_at": "2026-07-02T09:14:11Z",
"updated_at": "2026-08-15T07:41:03Z"
}
Error response404 · not_found
{
"type": "https://patternsradar.com/docs/api/errors#not_found",
"title": "not found",
"status": 404,
"detail": "not found",
"instance": "/v1/scans/42",
"code": "not_found",
"request_id": "6d0b7f3a9c214e08"
}

Delete a scan#

DELETE/v1/scans/:idAPI key

Delete a saved scan. Its alert, if any, goes with it.

Path parameters

idnumberrequired

The scan's id.

Response fields

okboolean

Always true — the scan and its alert are gone. A failure is an HTTP error, never ok: false.

Errors

Request
curl -s -X DELETE "https://api.patternsradar.com/v1/scans/42" \
-H "Authorization: Bearer $PATTERNSRADAR_API_KEY"
Response200
{ "ok": true }
Error response404 · not_found
{
"type": "https://patternsradar.com/docs/api/errors#not_found",
"title": "not found",
"status": 404,
"detail": "not found",
"instance": "/v1/scans/42",
"code": "not_found",
"request_id": "6d0b7f3a9c214e08"
}

Alerts#

Run a saved scan automatically after each session and get told what matched — by email, or on Max by webhook too. Alerts need a paid plan.

List alerts#

GET/v1/alertsAPI key

Every alert on the account, with the plan's alert allowances alongside.

Response fields

alertsobject[]

Every alert on the account, one object each.

alerts[].scan_idnumber

The saved scan this alert watches. Alerts have no id of their own — one scan carries at most one alert, addressed by the scan's id.

alerts[].namestring

The watched scan's name, for display.

alerts[].enabledboolean

Whether the alert runs in the nightly evaluation.

alerts[].trigger_mode"new_matches" | "any_match"

new_matches fires only when a symbol enters the result set; any_match fires whenever anything matches.

alerts[].channel_emailboolean

Whether matches are sent by email.

alerts[].webhook_urlstringonly when set

The URL matches are POSTed to. Absent when no webhook is configured.

alerts[].webhook_secretstringonly with a webhook

Keys the HMAC-SHA256 signature on every delivery — see the webhooks section. Travels only alongside a configured webhook_url, and never changes once generated.

alerts[].status"ok" | "broken" | "webhook_disabled"

The alert's health: ok runs normally, broken means the scan no longer compiles, webhook_disabled means the URL failed too many days running and was switched off.

alerts[].status_detailstringonly when broken

Why the alert is broken, in the compiler's words.

alerts[].last_evaluatedstringafter first run

When the nightly evaluation last considered this alert, RFC 3339 UTC. Absent until it has run once.

alerts[].updated_atstring

When the alert's configuration last changed, RFC 3339 UTC.

planstring

The account's plan.

limits.max_alert_scansnumber

How many scans the plan lets you alert on — compare with alerts.length before creating another.

limits.webhooks_enabledboolean

Whether the plan allows webhook delivery.

Errors

status is ok, broken (the scan no longer compiles — status_detail says why) or webhook_disabled (the URL failed too many days running and was switched off).
webhook_secret travels only alongside a configured webhook_url; it keys the delivery signature — see the webhooks section.
Request
curl -s "https://api.patternsradar.com/v1/alerts" \
-H "Authorization: Bearer $PATTERNSRADAR_API_KEY"
Response200
{
"alerts": [
{
"scan_id": 42,
"name": "Delivery surge",
"enabled": true,
"trigger_mode": "new_matches",
"channel_email": true,
"webhook_url": "https://example.com/hooks/patternsradar",
"webhook_secret": "9f2c47a1d85e03b6c4f8a92d1e7b5063f0a815c2d94e7b38",
"status": "ok",
"last_evaluated": "2026-08-14T12:45:00Z",
"updated_at": "2026-08-10T16:02:54Z"
}
],
"plan": "max",
"limits": { "max_alert_scans": 200, "webhooks_enabled": true }
}
Error response401 · unauthorized
{
"type": "https://patternsradar.com/docs/api/errors#unauthorized",
"title": "authentication required",
"status": 401,
"detail": "not signed in",
"instance": "/v1/alerts",
"code": "unauthorized",
"request_id": "6d0b7f3a9c214e08"
}

Alert history#

GET/v1/alerts/eventsAPI key

What the nightly evaluation found, newest first — including quiet nights where nothing fired.

Query parameters

scan_idnumberoptional

Only this scan's events. Omit for all scans.

limitnumberoptional

Maximum events to return.

Response fields

eventsobject[]

One event per alert per evaluated session, newest first — quiet nights included, so absence of matches is recorded, not silent.

events[].idnumber

The event's id, unique and ascending.

events[].scan_idnumber

The saved scan the event belongs to.

events[].namestring

The scan's name at evaluation time.

events[].data_datestring

The trading session that was evaluated, YYYY-MM-DD — the day the data describes, not the moment the evaluation ran.

events[].matchedstring[]

Every symbol the scan matched that session.

events[].new_matchesstring[]

The subset of matched that was not in the previous session's results — what a new_matches alert fires on.

events[].firedboolean

Whether notifications went out. false on a quiet night, or when trigger_mode is new_matches and everything that matched had matched before.

events[].created_atstring

When the evaluation ran, RFC 3339 UTC.

Errors

Request
curl -s "https://api.patternsradar.com/v1/alerts/events?scan_id=42&limit=10" \
-H "Authorization: Bearer $PATTERNSRADAR_API_KEY"
Response200
{
"events": [
{
"id": 918,
"scan_id": 42,
"name": "Delivery surge",
"data_date": "2026-08-14",
"matched": ["POLYCAB", "DIXON", "KEI"],
"new_matches": ["KEI"],
"fired": true,
"created_at": "2026-08-14T12:45:02Z"
}
]
}
Error response401 · unauthorized
{
"type": "https://patternsradar.com/docs/api/errors#unauthorized",
"title": "authentication required",
"status": 401,
"detail": "not signed in",
"instance": "/v1/alerts/events",
"code": "unauthorized",
"request_id": "6d0b7f3a9c214e08"
}

Webhook deliveries#

GET/v1/alerts/:id/deliveriesAPI key

The delivery log for one alert's webhook: every attempt, its status code and how long it took.

Path parameters

idnumberrequired

The saved scan's id the alert belongs to — there is no separate alert id.

Query parameters

limitnumberoptional

Maximum rows to return.

Response fields

deliveriesobject[]

Attempts, newest first — one row per HTTP request made.

deliveries[].idnumber

The attempt's id.

deliveries[].scan_idnumber

The saved scan whose alert made the delivery.

deliveries[].attemptnumber

1 for the first try. A failed delivery is retried on a backoff schedule, so one alert firing can log several rows with rising attempt numbers.

deliveries[].status_codenumbernullable

The HTTP status your receiver answered. null when no response arrived at all — a timeout or an unreachable host.

deliveries[].okboolean

true when the receiver answered 2xx.

deliveries[].errorstringnullable

null on success; otherwise a short classification of what went wrong — the note below lists the vocabulary.

deliveries[].duration_msnumber

How long the request took, milliseconds.

deliveries[].created_atstring

When the attempt was made, RFC 3339 UTC.

Errors

On a failed attempt error is a short classification — could not resolve host, connection refused, delivery timed out, receiver answered 500 — and status_code is null when no response arrived at all.
Request
curl -s "https://api.patternsradar.com/v1/alerts/42/deliveries?limit=10" \
-H "Authorization: Bearer $PATTERNSRADAR_API_KEY"
Response200
{
"deliveries": [
{
"id": 3311,
"scan_id": 42,
"attempt": 1,
"status_code": 200,
"ok": true,
"error": null,
"duration_ms": 184,
"created_at": "2026-08-14T12:45:03Z"
}
]
}
Error response401 · unauthorized
{
"type": "https://patternsradar.com/docs/api/errors#unauthorized",
"title": "authentication required",
"status": 401,
"detail": "not signed in",
"instance": "/v1/alerts/42/deliveries",
"code": "unauthorized",
"request_id": "6d0b7f3a9c214e08"
}

Create or update an alert#

PUT/v1/scans/:id/alertAPI key

Attach an alert to a saved scan, or change one that exists. A full replace: send the whole configuration each time.

Path parameters

idnumberrequired

The saved scan's id.

Body (JSON)

enabledbooleandefault true

Whether the alert runs at all.

trigger_mode"new_matches" | "any_match"required

new_matches fires only when a symbol enters the result set; any_match fires whenever the scan matches anything.

channel_emailbooleandefault true

Send matches by email.

webhook_urlstringoptional

HTTPS URL to POST matches to. Max plan only. Validated against private-address targets when saved.

Response fields

scan_idnumber

The saved scan this alert watches. Alerts have no id of their own — one scan carries at most one alert, addressed by the scan's id.

namestring

The watched scan's name, for display.

enabledboolean

Whether the alert runs in the nightly evaluation.

trigger_mode"new_matches" | "any_match"

new_matches fires only when a symbol enters the result set; any_match fires whenever anything matches.

channel_emailboolean

Whether matches are sent by email.

webhook_urlstringonly when set

The URL matches are POSTed to. Absent when no webhook is configured.

webhook_secretstringonly with a webhook

Keys the HMAC-SHA256 signature on every delivery — see the webhooks section. Travels only alongside a configured webhook_url, and never changes once generated.

status"ok" | "broken" | "webhook_disabled"

The alert's health: ok runs normally, broken means the scan no longer compiles, webhook_disabled means the URL failed too many days running and was switched off.

status_detailstringonly when broken

Why the alert is broken, in the compiler's words.

last_evaluatedstringafter first run

When the nightly evaluation last considered this alert, RFC 3339 UTC. Absent until it has run once.

updated_atstring

When the alert's configuration last changed, RFC 3339 UTC.

Errors

webhook_secret is generated when the webhook is first configured and never changes after, so a receiver that verified yesterday still verifies today. It signs every delivery — see the webhooks section for the scheme and receiver code.
Request
curl -s -X PUT "https://api.patternsradar.com/v1/scans/42/alert" \
-H "Authorization: Bearer $PATTERNSRADAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"enabled": true,
"trigger_mode": "new_matches",
"channel_email": true,
"webhook_url": "https://example.com/hooks/patternsradar"
}'
Response200
{
"scan_id": 42,
"name": "Delivery surge",
"enabled": true,
"trigger_mode": "new_matches",
"channel_email": true,
"webhook_url": "https://example.com/hooks/patternsradar",
"webhook_secret": "9f2c47a1d85e03b6c4f8a92d1e7b5063f0a815c2d94e7b38",
"status": "ok",
"updated_at": "2026-08-15T07:44:20Z"
}
Error response403 · limit_alerts_unavailable
{
"type": "https://patternsradar.com/docs/api/errors#limit_alerts_unavailable",
"title": "alerts are not available on this plan",
"status": 403,
"detail": "Alerting is part of the paid plans.",
"instance": "/v1/scans/42/alert",
"code": "limit_alerts_unavailable",
"request_id": "6d0b7f3a9c214e08"
}

Remove an alert#

DELETE/v1/scans/:id/alertAPI key

Detach the alert from a scan. The scan itself stays.

Path parameters

idnumberrequired

The saved scan's id.

Response fields

okboolean

Always true — the alert is gone. A failure is an HTTP error, never ok: false.

Errors

Request
curl -s -X DELETE "https://api.patternsradar.com/v1/scans/42/alert" \
-H "Authorization: Bearer $PATTERNSRADAR_API_KEY"
Response200
{ "ok": true }
Error response404 · not_found
{
"type": "https://patternsradar.com/docs/api/errors#not_found",
"title": "not found",
"status": 404,
"detail": "not found",
"instance": "/v1/scans/42/alert",
"code": "not_found",
"request_id": "6d0b7f3a9c214e08"
}

Webhooks#

On the Max plan, an alert can POST its matches to a URL you name. Every delivery is signed; verify the signature before trusting the body.

The delivery

Answer any 2xx promptly — the sender waits at most ten seconds and follows no redirects. A failed delivery is retried twice, about ten seconds and then two minutes later, with byte-identical body and headers. X-PatternsRadar-Delivery is stable across those retries: deduplicate on it. A URL that keeps failing for days is switched off and the alert’s status becomes webhook_disabled.

Verifying the signature

The signature is HMAC-SHA256 with the alert’s secret over the timestamp, a dot, and the raw body — hex-encoded behind a sha256= prefix: sha256=hex(hmac_sha256(secret, timestamp + "." + body)). Compare in constant time, and reject timestamps more than five minutes old to shut out replays.

The secret is created with the webhook and never changes afterwards, so a receiver that verified yesterday still verifies today. Read it on the alerts page, or from webhook_secret in the response of PUT /v1/scans/:id/alert and GET /v1/alerts. Treat it like any other credential: environment variable or secret store, never code.
Delivery · POST to your URL
POST <your URL>
Content-Type: application/json
User-Agent: PatternsRadar-Webhook/1
X-PatternsRadar-Event: scan.matched
X-PatternsRadar-Delivery: 918
X-PatternsRadar-Timestamp: 1786790703
X-PatternsRadar-Signature: sha256=6c1f...

{
"event": "scan.matched",
"scan_id": 42,
"scan_name": "Delivery surge",
"data_date": "2026-08-14",
"matched": ["POLYCAB", "DIXON", "KEI"],
"new_matches": ["KEI"],
"fired_at": "2026-08-14T12:45:02Z"
}
Verify the signature
# Verification needs HMAC — use a real language for the receiver.
# What arrives, for reference:
#
# POST <your URL>
# Content-Type: application/json
# X-PatternsRadar-Signature: sha256=<hex hmac of "timestamp.body">
# X-PatternsRadar-Timestamp: <unix seconds, signed with the body>
# X-PatternsRadar-Event: scan.matched
# X-PatternsRadar-Delivery: <id, stable across retries — dedupe on it>
#
# Recompute locally to check a captured delivery:
echo -n "${TIMESTAMP}.${BODY}" \
| openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" -hex

Account#

The account your key belongs to: who it is, what plan it is on, and whether alert emails are sent. Accounts themselves — signup, plans, keys — are managed in the browser.

Who am I#

GET/v1/auth/meAPI key

The authenticated account, its plan, the plan's limits and current usage — the call to make first when checking a key works.

Response fields

user.idnumber

The account's id.

user.emailstring

The address the account was registered with.

user.namestringnullable

The display name, if one was given at signup.

user.created_atstring

When the account was created, RFC 3339 UTC.

plan"free" | "pro" | "max"

The plan the key's requests are metered against.

plan_statusstring

The subscription's standing — active when the plan is in force. Anything else means payment is still settling, billing is paused, or the plan is winding down. Read limits rather than this to decide what the account may do.

limits.max_saved_scansnumber

How many scans the plan can keep.

limits.max_alert_scansnumber

How many scans can alert.

limits.webhooks_enabledboolean

Whether alerts may deliver by webhook.

limits.api_rate_per_minnumber

Requests per minute across the API.

limits.scan_rate_per_minnumber

Requests per minute for /v1/scan and /v1/hitrate, which cost more than the rest.

limits.max_api_keysnumber

How many active API keys the account may hold.

usage.saved_scansnumber

Scans currently saved — against limits.max_saved_scans.

usage.alert_scansnumber

Scans currently alerting — against limits.max_alert_scans.

Errors

Request
curl -s "https://api.patternsradar.com/v1/auth/me" \
-H "Authorization: Bearer $PATTERNSRADAR_API_KEY"
Response200
{
"user": {
"id": 7,
"email": "you@example.com",
"name": "You",
"created_at": "2026-08-15T07:50:00Z"
},
"plan": "pro",
"plan_status": "active",
"limits": {
"max_saved_scans": 100,
"max_alert_scans": 50,
"webhooks_enabled": false,
"api_rate_per_min": 300,
"scan_rate_per_min": 100,
"max_api_keys": 5
},
"usage": { "saved_scans": 12, "alert_scans": 3 }
}
Error response401 · unauthorized
{
"type": "https://patternsradar.com/docs/api/errors#unauthorized",
"title": "authentication required",
"status": 401,
"detail": "not signed in",
"instance": "/v1/auth/me",
"code": "unauthorized",
"request_id": "6d0b7f3a9c214e08"
}

Notification preferences#

GET/v1/me/notificationsAPI key

Whether alert emails are on for the account.

Response fields

email_alerts_enabledboolean

The account-wide switch for alert emails. When false, alerts still evaluate and webhooks still deliver — only the emails stop.

Errors

Request
curl -s "https://api.patternsradar.com/v1/me/notifications" \
-H "Authorization: Bearer $PATTERNSRADAR_API_KEY"
Response200
{ "email_alerts_enabled": true }
Error response401 · unauthorized
{
"type": "https://patternsradar.com/docs/api/errors#unauthorized",
"title": "authentication required",
"status": 401,
"detail": "not signed in",
"instance": "/v1/me/notifications",
"code": "unauthorized",
"request_id": "6d0b7f3a9c214e08"
}

Update notification preferences#

PATCH/v1/me/notificationsAPI key

Turn alert emails on or off account-wide. The unsubscribe link in every alert email flips the same switch, authenticated by its own signed token.

Body (JSON)

email_alerts_enabledbooleanrequired

Whether alert emails are sent at all.

Response fields

email_alerts_enabledboolean

The setting as it now stands, echoed back.

Errors

Request
curl -s -X PATCH "https://api.patternsradar.com/v1/me/notifications" \
-H "Authorization: Bearer $PATTERNSRADAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email_alerts_enabled": false
}'
Response200
{ "email_alerts_enabled": false }
Error response400 · validation
{
"type": "https://patternsradar.com/docs/api/errors#validation",
"title": "your request is invalid",
"status": 400,
"detail": "request body must be JSON",
"instance": "/v1/me/notifications",
"code": "validation",
"request_id": "6d0b7f3a9c214e08"
}

Error catalog#

Every failure the API can answer, one entry per type URI. Paste a response's type into a browser and land on its row; each endpoint's Errors list links here too.

your request is invalid

The request itself is malformed — a body that is not JSON, a missing field, an over-long query, an id that is not a number. detail names what was wrong.

Problem document400
{
"type": "https://patternsradar.com/docs/api/errors#validation",
"title": "your request is invalid",
"status": 400,
"detail": "request body must be JSON",
"instance": "/v1/scan",
"code": "validation",
"request_id": "6d0b7f3a9c214e08"
}

authentication required

No credential where one was needed: no session cookie, or no bearer key on a route that requires one.

Problem document401
{
"type": "https://patternsradar.com/docs/api/errors#unauthorized",
"title": "authentication required",
"status": 401,
"detail": "not signed in",
"instance": "/v1/scans",
"code": "unauthorized",
"request_id": "6d0b7f3a9c214e08"
}

your session has expired

A session cookie was sent but no longer matches a live session — it expired or was revoked. Sign in again.

Problem document401
{
"type": "https://patternsradar.com/docs/api/errors#session_expired",
"title": "your session has expired",
"status": 401,
"detail": "session has expired — sign in again",
"instance": "/v1/auth/me",
"code": "session_expired",
"request_id": "6d0b7f3a9c214e08"
}

that API key is not valid

A bearer key was sent but does not resolve to an active key — mistyped or revoked. The response carries a WWW-Authenticate header with error="invalid_token".

Problem document401
{
"type": "https://patternsradar.com/docs/api/errors#invalid_api_key",
"title": "that API key is not valid",
"status": 401,
"detail": "that API key is not valid — it may have been revoked",
"instance": "/v1/scans",
"code": "invalid_api_key",
"request_id": "6d0b7f3a9c214e08"
}

email or password is incorrect

Login refused. One message for an unknown address and a wrong password, deliberately, so the endpoint cannot be used to discover which addresses are registered.

Problem document401
{
"type": "https://patternsradar.com/docs/api/errors#invalid_credentials",
"title": "email or password is incorrect",
"status": 401,
"detail": "email or password is incorrect",
"instance": "/v1/auth/login",
"code": "invalid_credentials",
"request_id": "6d0b7f3a9c214e08"
}

not allowed

Authenticated, but this account may not do that. The generic case; the plan limits below are the specific ones.

Problem document403
{
"type": "https://patternsradar.com/docs/api/errors#forbidden",
"title": "not allowed",
"status": 403,
"detail": "not allowed",
"instance": "/v1/scans/42/alert",
"code": "forbidden",
"request_id": "6d0b7f3a9c214e08"
}

API keys cannot use this endpoint

The browser-only surface — key management, billing, signing out everywhere — refuses API keys: a leaked key must not be able to mint replacements for itself or touch billing. Sign in with a browser session instead.

Problem document403
{
"type": "https://patternsradar.com/docs/api/errors#session_only",
"title": "API keys cannot use this endpoint",
"status": 403,
"detail": "this endpoint is browser-only — sign in to do that",
"instance": "/v1/keys",
"code": "session_only",
"request_id": "6d0b7f3a9c214e08"
}

confirm your email address first

The account exists and is signed in, but its email address has never been confirmed. Two things need a confirmed address — switching on an alert, because we would be mailing an unproven address, and publishing a scan, because it becomes a public page. Everything else on the account works. POST /v1/auth/email/resend sends another link. This is not a plan limit and no upgrade clears it.

Problem document403
{
"type": "https://patternsradar.com/docs/api/errors#email_unverified",
"title": "confirm your email address first",
"status": 403,
"detail": "email_unverified",
"instance": "/v1/scan",
"code": "email_unverified",
"request_id": "6d0b7f3a9c214e08"
}

that link is not valid

A signed link — an email confirmation, a password reset — did not verify. Either it was altered in transit, it was truncated by a mail client that wrapped the URL, or it has already been used. Ask for a new one.

Problem document400
{
"type": "https://patternsradar.com/docs/api/errors#invalid_token",
"title": "that link is not valid",
"status": 400,
"detail": "invalid_token",
"instance": "/v1/scan",
"code": "invalid_token",
"request_id": "6d0b7f3a9c214e08"
}

that link has expired

The link verified but is past its lifetime: one hour for a password reset, three days for an email confirmation. Distinct from invalid_token on purpose, so the page can offer to send another rather than implying the link was wrong.

Problem document410
{
"type": "https://patternsradar.com/docs/api/errors#expired_token",
"title": "that link has expired",
"status": 410,
"detail": "expired_token",
"instance": "/v1/scan",
"code": "expired_token",
"request_id": "6d0b7f3a9c214e08"
}

saved-scan limit reached

The account already keeps as many scans as the plan allows. Existing scans stay readable, runnable and deletable.

Problem document403
{
"type": "https://patternsradar.com/docs/api/errors#limit_saved_scans",
"title": "saved-scan limit reached",
"status": 403,
"detail": "You have reached the number of scans your plan can keep.",
"instance": "/v1/scans",
"code": "limit_saved_scans",
"request_id": "6d0b7f3a9c214e08"
}

alerts are not available on this plan

Alerting is part of the paid plans.

Problem document403
{
"type": "https://patternsradar.com/docs/api/errors#limit_alerts_unavailable",
"title": "alerts are not available on this plan",
"status": 403,
"detail": "Alerting is part of the paid plans.",
"instance": "/v1/scans/42/alert",
"code": "limit_alerts_unavailable",
"request_id": "6d0b7f3a9c214e08"
}

alerted-scan limit reached

As many scans are already alerting as the plan allows.

Problem document403
{
"type": "https://patternsradar.com/docs/api/errors#limit_alert_scans",
"title": "alerted-scan limit reached",
"status": 403,
"detail": "You are already alerting on as many scans as your plan allows.",
"instance": "/v1/scans/42/alert",
"code": "limit_alert_scans",
"request_id": "6d0b7f3a9c214e08"
}

webhook delivery is not available on this plan

Webhook delivery is part of the Max plan.

Problem document403
{
"type": "https://patternsradar.com/docs/api/errors#limit_webhooks",
"title": "webhook delivery is not available on this plan",
"status": 403,
"detail": "Webhooks are part of the Max plan.",
"instance": "/v1/scans/42/alert",
"code": "limit_webhooks",
"request_id": "6d0b7f3a9c214e08"
}

API key limit reached

The account holds as many active keys as the plan allows. Revoke one to make room.

Problem document403
{
"type": "https://patternsradar.com/docs/api/errors#limit_api_keys",
"title": "API key limit reached",
"status": 403,
"detail": "You have as many API keys as your plan allows. Revoke one to make room.",
"instance": "/v1/keys",
"code": "limit_api_keys",
"request_id": "6d0b7f3a9c214e08"
}

the MCP server is not available on this plan

The plan does not include Model Context Protocol access. Every current plan does; a plan can be configured without it.

Problem document403
{
"type": "https://patternsradar.com/docs/api/errors#limit_mcp",
"title": "the MCP server is not available on this plan",
"status": 403,
"detail": "The MCP server is not included in your plan.",
"instance": "/mcp",
"code": "limit_mcp",
"request_id": "6d0b7f3a9c214e08"
}

API access is not available on this plan

The plan does not include calling the API with a bearer key. Only key-authenticated requests are refused — the app itself keeps working, because it signs in with a session.

Problem document403
{
"type": "https://patternsradar.com/docs/api/errors#limit_rest_api",
"title": "API access is not available on this plan",
"status": 403,
"detail": "API access with a key is not included in your plan.",
"instance": "/v1/scans",
"code": "limit_rest_api",
"request_id": "6d0b7f3a9c214e08"
}

today's scan allowance is spent

The account has run as many scans today as the plan allows. Distinct from rate_limited, which is a 429 meaning "slow down and try again in a moment": this is the day's budget, and the fix is a different plan or tomorrow, not a retry. Anonymous callers never see it — they have no plan and no daily allowance, only the per-minute limit on their address.

Problem document403
{
"type": "https://patternsradar.com/docs/api/errors#limit_scans_per_day",
"title": "today's scan allowance is spent",
"status": 403,
"detail": "You have run as many scans today as your plan allows.",
"instance": "/v1/scan",
"code": "limit_scans_per_day",
"request_id": "6d0b7f3a9c214e08"
}

today's replay allowance is spent

The account has run as many hit-rate replays today as the plan allows. Its own allowance because a replay costs several times what a scan does, so spending one budget does not spend the other.

Problem document403
{
"type": "https://patternsradar.com/docs/api/errors#limit_hitrate_per_day",
"title": "today's replay allowance is spent",
"status": 403,
"detail": "You have run as many replays today as your plan allows.",
"instance": "/v1/hitrate",
"code": "limit_hitrate_per_day",
"request_id": "6d0b7f3a9c214e08"
}

that scan has been taken down

A published scan an operator has withdrawn. Not a plan limit — no upgrade lifts it, so the client must not offer one — and the detail carries the reason, which is meant to be shown to the author.

Problem document403
{
"type": "https://patternsradar.com/docs/api/errors#scan_taken_down",
"title": "that scan has been taken down",
"status": 403,
"detail": "impersonates a registered investment adviser",
"instance": "/v1/scans/42/visibility",
"code": "scan_taken_down",
"request_id": "6d0b7f3a9c214e08"
}

that coupon code is not valid

No such code, or it does not apply to the plan being bought, or it has been fully redeemed, or this account has already used it. The detail says which.

Problem document400
{
"type": "https://patternsradar.com/docs/api/errors#invalid_coupon",
"title": "that coupon code is not valid",
"status": 400,
"detail": "that coupon code is not valid",
"instance": "/v1/billing/coupon/preview",
"code": "invalid_coupon",
"request_id": "6d0b7f3a9c214e08"
}

that coupon has expired

The code was real and its window has closed. Distinct from invalid_coupon because there is nothing the customer can do about it.

Problem document410
{
"type": "https://patternsradar.com/docs/api/errors#coupon_expired",
"title": "that coupon has expired",
"status": 410,
"detail": "that coupon has expired",
"instance": "/v1/billing/coupon/preview",
"code": "coupon_expired",
"request_id": "6d0b7f3a9c214e08"
}

that plan is no longer available

The plan has been withdrawn from sale. Accounts already on it keep it and keep their limits; it simply takes no new subscribers.

Problem document409
{
"type": "https://patternsradar.com/docs/api/errors#plan_archived",
"title": "that plan is no longer available",
"status": 409,
"detail": "that plan is no longer offered — see the current plans",
"instance": "/v1/billing/subscribe",
"code": "plan_archived",
"request_id": "6d0b7f3a9c214e08"
}

not found

No such route, symbol, scan, alert or key. Ownership is not disclosed: something that exists but belongs to somebody else answers the same way.

Problem document404
{
"type": "https://patternsradar.com/docs/api/errors#not_found",
"title": "not found",
"status": 404,
"detail": "not found",
"instance": "/v1/scans/42",
"code": "not_found",
"request_id": "6d0b7f3a9c214e08"
}

conflict

The request collides with current state — a taken email, a duplicate scan name, a subscription already on that plan, nothing active to cancel.

Problem document409
{
"type": "https://patternsradar.com/docs/api/errors#conflict",
"title": "conflict",
"status": 409,
"detail": "conflict",
"instance": "/v1/scans",
"code": "conflict",
"request_id": "6d0b7f3a9c214e08"
}

too many requests

Over the per-minute allowance. The body is the plain problem document; the wait is in the Retry-After header, and the numbers per plan are in the rate-limits section.

Problem document429
{
"type": "https://patternsradar.com/docs/api/errors#rate_limited",
"title": "too many requests",
"status": 429,
"detail": "too many requests — slow down or upgrade your plan",
"instance": "/v1/scan",
"code": "rate_limited",
"request_id": "6d0b7f3a9c214e08"
}

the request could not be completed

Something went wrong on our side. The response says no more than that; quote the request_id when reporting it, and the request may be retried.

Problem document500
{
"type": "https://patternsradar.com/docs/api/errors#internal",
"title": "the request could not be completed",
"status": 500,
"detail": "the request could not be completed",
"instance": "/v1/scan",
"code": "internal",
"request_id": "6d0b7f3a9c214e08"
}

an upstream provider failed

A provider the request depends on did not answer usefully. Safe to retry; if it persists, wait a little and try again.

Problem document502
{
"type": "https://patternsradar.com/docs/api/errors#upstream",
"title": "an upstream provider failed",
"status": 502,
"detail": "could not start the subscription — please try again",
"instance": "/v1/billing/subscribe",
"code": "upstream",
"request_id": "6d0b7f3a9c214e08"
}

billing is not configured on this deployment

Subscription routes are present but cannot act on this deployment.

Problem document503
{
"type": "https://patternsradar.com/docs/api/errors#billing_unconfigured",
"title": "billing is not configured on this deployment",
"status": 503,
"detail": "subscriptions are not available on this deployment",
"instance": "/v1/billing/subscribe",
"code": "billing_unconfigured",
"request_id": "6d0b7f3a9c214e08"
}