PiQ Developer API
Put PiQ market headlines on your site, app or terminal. The API returns our categories and the latest stories in each one: headline, image and a link. Readers follow the link to PiQ for the full story.
Getting an API key
API access is by invitation. Email [email protected] with your company name, what you plan to build, and roughly how many requests you expect. We'll send you a key with rate limits and quotas set for your use case.
Keep your key secret. Call the API from your server, not from browser or mobile app code where users could read the key. If a key leaks, tell us and we'll rotate it.
Authentication
Send your key with every request, in the x-api-key header:
curl https://piqmarkets.com/api/v1/categories \
-H "x-api-key: pqm_your_key_here"
Authorization: Bearer pqm_your_key_here works as well.
Base URL
https://piqmarkets.com/api/v1
All responses are JSON. All timestamps are ISO 8601 in UTC.
Endpoints
List categories
GET /api/v1/categories
Returns every category you can filter stories by.
{
"items": [
{ "slug": "equities-funds", "name": "Equities & Funds" },
{ "slug": "crypto-digital-assets", "name": "Crypto & Digital Assets" }
]
}
List stories
GET /api/v1/stories
Returns live stories, newest first by default.
| Parameter | Description |
|---|---|
category | One or more category slugs, comma-separated (equities-funds,macro-rates-fx). Leave it out to get all categories. |
sort | latest (default) orders by publish time. top returns our highest-impact stories first. |
limit | Stories per page, 1–50. Default 20. |
offset | Number of stories to skip, for paging. Default 0. |
since | ISO timestamp. Only return stories published after it. Useful for polling. |
curl "https://piqmarkets.com/api/v1/stories?category=equities-funds&limit=2" \
-H "x-api-key: pqm_your_key_here"
{
"items": [
{
"id": "4b8ad6a1-0082-467f-b43a-c699f0a279e9",
"headline": "Shein shares slide over 5% on second day of Hong Kong trading",
"image": "https://cdn.example.com/shein.jpg",
"category": { "slug": "equities-funds", "name": "Equities & Funds" },
"publishedAt": "2026-09-02T08:50:02.717Z",
"link": "https://piqmarkets.com/story/shein-shares-slide-over-5-on-second-day-of-hong-kong-trading?utm_source=your-company&utm_medium=api&utm_campaign=partner-api"
}
],
"limit": 2,
"offset": 0,
"hasMore": true
}
image can be null. When hasMore is true, ask for the next page with offset + limit.
Get one story
GET /api/v1/stories/{id}
{id} is a story's id, or the slug from its link. Returns a single story in the same shape as the list items above.
Rate limits and quotas
Each key has its own limits:
- a per-minute rate limit
- a daily quota (resets at 00:00 UTC)
- a monthly quota (resets on the 1st, 00:00 UTC)
Responses tell you where you stand, for each limit your key has:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests allowed per minute |
X-RateLimit-Remaining | Requests left in the current minute |
X-Quota-Daily-Remaining | Requests left today |
X-Quota-Monthly-Remaining | Requests left this month |
Going over a limit returns 429 Too Many Requests with a Retry-After header, in seconds:
{ "error": "rate_limited", "retryAfterSeconds": 12 }
{
"error": "quota_exceeded",
"scope": "daily",
"resetAt": "2026-09-22T00:00:00.000Z"
}
Requests rejected with a 429 don't count toward your quota. Stories change every few minutes, so polling once a minute is plenty. Responses are cached for up to 30 seconds.
Errors
Errors come back with an HTTP status and a JSON body like { "error": "code" }.
| Status | Code | Meaning |
|---|---|---|
| 400 | unknown_category | A category slug doesn't exist. Get the valid slugs from /categories. |
| 400 | invalid_since | since isn't a valid timestamp. |
| 401 | invalid_api_key | The key is missing, wrong, or has been revoked. |
| 401 | api_key_expired | The key has passed its expiry date. |
| 404 | not_found | No story with that id or slug. |
| 429 | rate_limited | Per-minute limit reached. Wait Retry-After seconds. |
| 429 | quota_exceeded | Daily or monthly quota used up. |
| 500 | internal_error | Something went wrong on our side. Try again shortly. |