|
1 |
+// /limits — Access & limits. The platform is free for everyone; three access levels differ only by limits. |
|
2 |
+import React from 'react' |
|
3 |
+import { Link } from 'react-router-dom' |
|
4 |
+import { CONTACT_EMAIL, HIGH_USAGE_MAILTO, TIERS } from '../../app/api.js' |
|
5 |
+import Callout from '../../components/Callout.jsx' |
|
6 |
+import Code from '../../components/Code.jsx' |
|
7 |
+import { BoltIcon, CheckIcon, KeyIcon, MailIcon } from '../../components/Icons.jsx' |
|
8 |
+import useTitle from '../../docs/useTitle.js' |
|
9 |
+import './limits.css' |
|
10 |
+ |
|
11 |
+const fmt = n => n.toLocaleString('en-US') |
|
12 |
+const windowLabel = w => (w === 'hour' ? 'per hour' : 'per minute') |
|
13 |
+ |
|
14 |
+const FAQ = [ |
|
15 |
+ { q: 'Is it really all free?', a: 'Yes. Every dataset, endpoint and access level is free. The three levels differ only in how much you can pull per window — and the highest one is granted on request, by e-mail.' }, |
|
16 |
+ { q: 'How are requests counted?', a: 'Each HTTP request counts once against the requests window, whatever it returns. Two expensive endpoints (fundamentals screener and frames) count twice. A 304 Not Modified response counts zero.' }, |
|
17 |
+ { q: 'How are rows counted?', a: 'The number of data rows in the response (the X-Row-Count header) is deducted from the rows window after the response is produced. Parquet responses count half. Bulk downloads (/v1/bulk/*) count zero. Metadata endpoints return no rows.' }, |
|
18 |
+ { q: 'What is a "window"?', a: 'A sliding window evaluated at one-second resolution: one hour for keyless access, one minute for keyed access. If you use your budget in the first seconds, you wait until enough of it has slid out — X-RateLimit-Reset tells you when.' }, |
|
19 |
+ { q: 'Which headers tell me where I stand?', a: 'X-RateLimit-Limit-Requests, X-RateLimit-Remaining-Requests, X-RateLimit-Limit-Rows, X-RateLimit-Remaining-Rows, X-RateLimit-Reset and X-Row-Count are on every response. GET /v1/limits returns the same information as JSON.' }, |
|
20 |
+ { q: 'What happens on 429?', a: 'The request is not executed. The body is the standard error envelope with code RATE_LIMIT_EXCEEDED and a type telling you which budget ran out (requests_per_hour, rows_per_minute…). Wait Retry-After seconds and retry. Other 4xx errors are not transient — fix the request instead of retrying.' }, |
|
21 |
+ { q: 'What is "max rows per request"?', a: 'The largest limit= value your level accepts on a single call. Asking for more returns 400 ROW_LIMIT_EXCEEDED. Use CSV (up to 2 000 000 rows on v1 bars), Parquet, pagination with cursor, or bulk downloads for larger extracts.' }, |
|
22 |
+ { q: 'I am behind a corporate network and hit the keyless limit immediately.', a: 'Keyless access is metered per IP, and a shared egress means a shared budget. Create a free account: keys are metered individually.' }, |
|
23 |
+ { q: 'How fast is the high-usage request handled?', a: 'Send the e-mail from the address of your account with a sentence on what you are building and the expected volume. The higher limits are applied to your existing keys; nothing changes in your code.' }, |
|
24 |
+] |
|
25 |
+ |
|
26 |
+export default function Limits() { |
|
27 |
+ useTitle('Access & limits — free for everyone', 'HF Market Data is free. Keyless access is very limited, a free account gives much higher limits, and higher limits are granted on request by e-mail.') |
|
28 |
+ return ( |
|
29 |
+ <main className="page limits" data-testid="limits-page"> |
|
30 |
+ <header className="limits-head"> |
|
31 |
+ <p className="eyebrow">Access & limits</p> |
|
32 |
+ <h1>Free for everyone.</h1> |
|
33 |
+ <p className="lead">There is nothing to buy. Keyless access is deliberately small so you can try things without signing up; a free account raises the limits a lot; and if you need more, you simply ask.</p> |
|
34 |
+ <div className="limits-cta"> |
|
35 |
+ <Link to="/signup" className="btn btn-primary btn-lg" data-testid="cta-signup"><KeyIcon /> Create free account</Link> |
|
36 |
+ <a href={HIGH_USAGE_MAILTO} className="btn btn-lg" data-testid="cta-high-usage"><MailIcon /> Request higher limits → {CONTACT_EMAIL}</a> |
|
37 |
+ </div> |
|
38 |
+ </header> |
|
39 |
+ |
|
40 |
+ <section aria-labelledby="tiers-heading"> |
|
41 |
+ <h2 id="tiers-heading" className="sr-only">Access levels</h2> |
|
42 |
+ <div className="tiers" data-testid="tiers"> |
|
43 |
+ {TIERS.map((t, i) => ( |
|
44 |
+ <article key={t.id} className={`tier ${i === 1 ? 'tier-featured' : ''}`} data-testid={`tier-${t.id}`}> |
|
45 |
+ <header> |
|
46 |
+ <h3>{['Keyless', 'Free account', 'High usage'][i]}</h3> |
|
47 |
+ <p className="muted">{['Per IP · nothing to do', 'API key · sign up with an e-mail', 'API key · on request, free'][i]}</p> |
|
48 |
+ </header> |
|
49 |
+ <dl className="tier-numbers"> |
|
50 |
+ <div><dt>Requests</dt><dd><strong>{fmt(t.requests)}</strong> <span>{windowLabel(t.window)}</span></dd></div> |
|
51 |
+ <div><dt>Rows</dt><dd><strong>{fmt(t.rows)}</strong> <span>{windowLabel(t.window)}</span></dd></div> |
|
52 |
+ <div><dt>Max rows / request</dt><dd><strong>{fmt(t.maxRows)}</strong></dd></div> |
|
53 |
+ <div><dt>Window</dt><dd><strong>1 {t.window}</strong> <span>sliding</span></dd></div> |
|
54 |
+ <div><dt>Datasets</dt><dd><strong>All</strong> <span>same data at every level</span></dd></div> |
|
55 |
+ <div><dt>Cost</dt><dd><strong>Free</strong></dd></div> |
|
56 |
+ </dl> |
|
57 |
+ <footer> |
|
58 |
+ {i === 0 && <Link to="/playground" className="btn">Try it keyless</Link>} |
|
59 |
+ {i === 1 && <Link to="/signup" className="btn btn-primary">Create free account</Link>} |
|
60 |
+ {i === 2 && <a href={HIGH_USAGE_MAILTO} className="btn">Request by e-mail</a>} |
|
61 |
+ </footer> |
|
62 |
+ </article> |
|
63 |
+ ))} |
|
64 |
+ </div> |
|
65 |
+ <p className="muted small">The same numbers are published by the API in <code>info.x-tiers</code> of <a href="/openapi.json">openapi.json</a> and returned for your own key by <code>GET /v1/limits</code>.</p> |
|
66 |
+ </section> |
|
67 |
+ |
|
68 |
+ <section className="limits-incentives"> |
|
69 |
+ <Callout type="tip" title="Three ways to stretch any level"> |
|
70 |
+ <ul> |
|
71 |
+ <li><strong>Parquet responses count half</strong> — <code>format=parquet</code> on v2 endpoints is charged 50 % of the rows returned.</li> |
|
72 |
+ <li><strong>Bulk downloads are outside the rows quota</strong> — <code>/v1/bulk/*</code> whole-universe Parquet extracts count as one request and zero rows.</li> |
|
73 |
+ <li><strong>304 Not Modified is free</strong> — send <code>If-None-Match</code> with the ETag you received; unchanged data costs nothing.</li> |
|
74 |
+ </ul> |
|
75 |
+ </Callout> |
|
76 |
+ </section> |
|
77 |
+ |
|
78 |
+ <section className="limits-headers"> |
|
79 |
+ <h2>Every response tells you where you stand</h2> |
|
80 |
+ <div className="grid grid-2"> |
|
81 |
+ <Code language="http" title="Response headers" copy={false} code={`HTTP/1.1 200 OK |
|
82 |
+X-RateLimit-Limit-Requests: 120 |
|
83 |
+X-RateLimit-Remaining-Requests: 118 |
|
84 |
+X-RateLimit-Limit-Rows: 1000000 |
|
85 |
+X-RateLimit-Remaining-Rows: 994800 |
|
86 |
+X-RateLimit-Reset: 1757030460 |
|
87 |
+X-Row-Count: 5000`} /> |
|
88 |
+ <Code language="json" title="429 Too Many Requests" copy={false} code={JSON.stringify({ error: { code: 'RATE_LIMIT_EXCEEDED', message: 'Request quota exhausted: 30 requests per hour for keyless access. Create a free account for 120 requests per minute.', docs: 'https://www.hfmarketdata.io/docs/errors#rate_limit_exceeded', type: 'requests_per_hour' }, detail: 'Request quota exhausted: 30 requests per hour for keyless access.' }, null, 2)} /> |
|
89 |
+ </div> |
|
90 |
+ <p className="muted">Handling code in curl, Python, JavaScript and R is in the <Link to="/docs/rate-limits">rate limits guide</Link>; every error code is on the <Link to="/docs/errors">errors page</Link>.</p> |
|
91 |
+ </section> |
|
92 |
+ |
|
93 |
+ <section className="limits-faq" aria-labelledby="faq-heading"> |
|
94 |
+ <h2 id="faq-heading">Questions</h2> |
|
95 |
+ <div className="faq"> |
|
96 |
+ {FAQ.map(f => ( |
|
97 |
+ <details key={f.q}> |
|
98 |
+ <summary>{f.q}</summary> |
|
99 |
+ <p>{f.a}</p> |
|
100 |
+ </details> |
|
101 |
+ ))} |
|
102 |
+ </div> |
|
103 |
+ </section> |
|
104 |
+ |
|
105 |
+ <section className="limits-final"> |
|
106 |
+ <div className="card limits-final-card"> |
|
107 |
+ <BoltIcon /> |
|
108 |
+ <div> |
|
109 |
+ <h3>Need more than the free account gives you?</h3> |
|
110 |
+ <p className="muted">Write to <a href={HIGH_USAGE_MAILTO}>{CONTACT_EMAIL}</a> with what you are building. Higher limits are switched on your existing keys — free, on request.</p> |
|
111 |
+ </div> |
|
112 |
+ <ul className="checks"> |
|
113 |
+ <li><CheckIcon /> 600 requests / minute</li> |
|
114 |
+ <li><CheckIcon /> 10 000 000 rows / minute</li> |
|
115 |
+ <li><CheckIcon /> 200 000 rows / request</li> |
|
116 |
+ </ul> |
|
117 |
+ </div> |
|
118 |
+ </section> |
|
119 |
+ </main> |
|
120 |
+ ) |
|
121 |
+} |