Newport Exchange
Developers

Newport Exchange API

A JSON-over-HTTPS REST API for crypto news posts, market listings and daily price history. Authenticated news endpoints and public market-data endpoints share one base URL.

Base URLhttps://api.newportexchange.org/v2Last updated
On this page

Overview

The Newport Exchange API serves the same data that powers Market and News on this site. All endpoints are read-only GET requests that return JSON over HTTPS, and every URL in this reference is relative to https://api.newportexchange.org/v2.

  • Posts (/posts) — curated crypto news with search and filters. Requires an API key.
  • Market data (/crypto/…) — top coins by market capitalisation with price, volume, supply and percentage change. Public.
  • Historical data (/crypto/historical/…, /crypto-history/…) — daily OHLCV candles. Public.
  • News (/news) — the public news feed used by this website. Public.

Timestamps in /posts responses are ISO 8601 in UTC. Public endpoints return the underlying record shapes unchanged, so some use unix seconds instead — each section says which.

Quickstart

  1. Request an API key and store it in an environment variable on your server, e.g. NEX_API_KEY.
  2. Send it in the X-API-Key header and fetch the five latest posts that mention Bitcoin:
curl -s "https://api.newportexchange.org/v2/posts?symbol=BTC&limit=5" \
  -H "X-API-Key: YOUR_API_KEY"

The market-data, historical and news endpoints need no key — try curl https://api.newportexchange.org/v2/crypto/BTC.

Authentication

API keys are issued by Newport Exchange to registered users. There is no self-serve dashboard yet: email [email protected] to request a key, with your name, organisation and intended use. Keys look like nex_live_… and are shown to you once — store them in a secrets manager.

Send the key in the X-API-Key request header on every call to an authenticated endpoint. Never put it in the query string: URLs end up in proxy logs, browser history and analytics.

Request header
GET /v2/posts HTTP/1.1
Host: api.newportexchange.org
X-API-Key: YOUR_API_KEY

Authentication failures return:

Authentication errors
StatusTypeDescription
401API key requiredThe X-API-Key header is missing or empty.
401Invalid API keyThe key is malformed, unknown or has been revoked.
403API key lacks the required scopeThe key is valid but not permitted to read this resource. Contact us to change its scopes.
429Too many invalid API key attemptsToo many 401s from your IP address within a minute. Fix the key before retrying.
Missing key
HTTP/1.1 401 Unauthorized
Content-Type: application/json

{"success":false,"message":"API key required"}
Unknown or revoked key
HTTP/1.1 401 Unauthorized
Content-Type: application/json

{"success":false,"message":"Invalid API key"}

Rate limits

Limits are counted in fixed one-minute windows:

  • Per API key on authenticated endpoints (/posts): 60 requests per minute by default, configurable per key — ask when you request one. The budget is shared by the list and single-post endpoints and is counted per key, not per IP, so several servers using one key share it. Requests with a valid key are not subject to the per-IP limit.
  • Per IP address on the public endpoints: 300 requests per minute by default. When exceeded, the message is Too many requests — please slow down.

Responses carry the IETF draft RateLimit header fields so you can pace yourself before you hit the limit:

Rate limit response headers
HeaderTypeDescription
RateLimit-LimitintegerRequests allowed in the current window.
RateLimit-RemainingintegerRequests left in the current window.
RateLimit-ResetintegerSeconds until the window resets.
Headers on a normal response
HTTP/1.1 200 OK
RateLimit-Limit: 60
RateLimit-Remaining: 57
RateLimit-Reset: 42

Over the limit, the API returns 429. Stop sending requests until RateLimit-Reset seconds have passed, then retry with exponential backoff and jitter — see Retries and backoff.

Response 429
HTTP/1.1 429 Too Many Requests
RateLimit-Limit: 60
RateLimit-Remaining: 0
RateLimit-Reset: 18

{"success":false,"message":"Rate limit exceeded"}

Errors

Errors use standard HTTP status codes and a consistent JSON body: success is always false, message is a human-readable summary, and validation errors add a details array naming each offending field. Match on the status code, not on message text.

Response 400
{
  "success": false,
  "message": "Invalid query parameters",
  "details": [
    { "field": "limit", "issue": "must be an integer between 1 and 100" }
  ]
}
HTTP status codes
StatusTypeDescription
200OKSuccess.
400Bad RequestA parameter is missing, malformed or out of range. Fix the request; do not retry.
401UnauthorizedMissing, unknown or revoked API key.
403ForbiddenThe API key is valid but lacks the scope for this endpoint.
404Not FoundThe resource (post, currency, article) or route does not exist.
429Too Many RequestsRate limit exceeded. Back off until the window resets.
500Internal Server ErrorUnexpected server error. Safe to retry with backoff.
503Service UnavailableA dependency such as the database is temporarily unavailable. Retry with backoff.

Pagination

List endpoints are paginated with page (1-based) and limit. There is deliberately no total count — counting a filtered result set is expensive and goes stale while you page.

  • /posts returns meta.has_more. Request the next page while it is true; stop when it is false.
  • /news returns a bare array and echoes the page in the X-Page and X-Per-Page response headers. Stop when a page comes back with fewer items than limit.

When polling for new content, prefer a time filter over deep page numbers — see Polling for new posts.

Posts

Posts are crypto news articles collected from established publishers, normalised into a clean shape with a plain-text body. Both endpoints require an API key and are sent with Cache-Control: private, max-age=60.

List posts

GET/v2/postsAPI key required

Returns published posts, newest first by default (ties broken by id), as data plus a meta object with the page, limit, has_more and the normalised filters that were applied — from/to are echoed as ISO 8601, and sort and include_body are always present.

curl -s -G "https://api.newportexchange.org/v2/posts" \
  -H "X-API-Key: YOUR_API_KEY" \
  --data-urlencode "q=etf" \
  --data-urlencode "symbol=BTC" \
  --data-urlencode "from=2026-09-01" \
  --data-urlencode "sort=newest" \
  --data-urlencode "limit=20" \
  --data-urlencode "page=1"
Response 200
{
  "data": [
    {
      "id": 123,
      "slug": "bitcoin-rallies-as-etf-inflows-accelerate",
      "title": "Bitcoin rallies as ETF inflows accelerate",
      "excerpt": "Spot bitcoin ETFs recorded their strongest week of inflows since...",
      "source": "CoinDesk",
      "image": "https://images.example.com/btc-rally.jpg",
      "source_url": "https://www.coindesk.com/markets/2026/09/25/…",
      "published_at": "2026-09-25T14:03:00.000Z"
    }
  ],
  "meta": {
    "page": 1,
    "limit": 20,
    "has_more": true,
    "filters": {
      "q": "etf",
      "symbol": "BTC",
      "from": "2026-09-01T00:00:00.000Z",
      "sort": "newest",
      "include_body": false
    }
  }
}

Filters

All query parameters are optional and can be combined. Unknown parameters are ignored; each parameter may appear only once.

Query parameters for GET /v2/posts
ParameterTypeDefaultDescription
pageinteger 1–100001Page number.
limitinteger 1–10020Posts per page.
qstring ≤ 100 chars—Case-insensitive substring search in the title and body. Matched literally (no wildcards).
symbolstring—Coin ticker, e.g. BTC (1–15 letters or digits, case-insensitive). Returns posts whose title or body contains the ticker as a whole word. It does not match the coin’s name (ETH will not find a story that only says “Ethereum” — combine with q if you need both), and tickers that are also English words (ONE, GAS) match those words too.
sourcestring ≤ 100 chars—Publisher or author name, exact match, case-insensitive, e.g. CoinDesk.
fromunix seconds | ISO 8601—Only posts with published_at at or after this time (inclusive), e.g. 1758808980, 2026-09-01 (midnight UTC) or 2026-09-01T12:00:00Z.
tounix seconds | ISO 8601—Only posts with published_at at or before this time (inclusive). A date without a time means the end of that UTC day, so from=2026-09-01&to=2026-09-01 covers the whole day. Must not be earlier than from.
sortnewest | oldestnewestOrder by published_at.
include_bodytrue | falsefalseInclude the full body of each post. Leave it off for list views to keep responses small.

Invalid values return 400 with a details entry per offending field (see Errors).

Get a single post

GET/v2/posts/:slugAPI key required

Returns one post by its slug, wrapped in data. The body is always included. Unknown or unpublished slugs return 404; a slug that is not 1–190 characters of A–Z a–z 0–9 . _ - returns 400 (Invalid slug).

curl -s "https://api.newportexchange.org/v2/posts/bitcoin-rallies-as-etf-inflows-accelerate" \
  -H "X-API-Key: YOUR_API_KEY"
Response 200
{
  "data": {
    "id": 123,
    "slug": "bitcoin-rallies-as-etf-inflows-accelerate",
    "title": "Bitcoin rallies as ETF inflows accelerate",
    "excerpt": "Spot bitcoin ETFs recorded their strongest week of inflows since...",
    "body": "Spot bitcoin ETFs recorded their strongest week of inflows since…\n\nAnalysts attributed the move to…",
    "source": "CoinDesk",
    "image": "https://images.example.com/btc-rally.jpg",
    "source_url": "https://www.coindesk.com/markets/2026/09/25/…",
    "published_at": "2026-09-25T14:03:00.000Z"
  }
}
Response 404
{"success":false,"message":"Post not found"}

The post object

Fields of a post
FieldTypeDescription
idintegerStable numeric identifier.
slugstringURL-safe identifier; use it with /posts/:slug.
titlestringHeadline.
excerptstringPlain-text summary: the start of the body, cut at a word boundary to about 280 characters and ending in "..." when shortened.
bodystringFull article as plain text, paragraphs separated by \n\n, no links or markup. Only with include_body=true on the list endpoint; always on the single-post endpoint.
sourcestring | nullPublisher or author name — the value the source filter matches.
imagestring (URL) | nullLead image URL.
source_urlstring (URL) | nullLink to the original article. Attribute the publisher when you display a post.
published_atstring (ISO 8601, UTC) | nullPublication time, e.g. 2026-09-25T14:03:00.000Z.

Market data

Current listings for the top 100 coins by market capitalisation, sourced from CoinMarketCap. These endpoints are public, rate limited per IP, and need no key. Responses are unwrapped (a bare array or object, no data envelope) and sent with Cache-Control: public, max-age=30, stale-while-revalidate=60.

List currencies by rank

GET/v2/crypto/currencies?rankFrom=1&rankTo=10Public · no key · rate limited per IP

Returns the coins whose current rank is between rankFrom and rankTo inclusive, ordered by rank. Both parameters are required — without them the path is treated as a symbol lookup and returns 404. Coins that dropped out of the latest listing are excluded, so every rank appears at most once.

Query parameters for GET /v2/crypto/currencies
ParameterTypeDefaultDescription
rankFromrequiredinteger—Lowest rank to include, e.g. 1.
rankTorequiredinteger—Highest rank to include, e.g. 100. Listings cover the top 100.
curl -s "https://api.newportexchange.org/v2/crypto/currencies?rankFrom=1&rankTo=10"

Get a single currency

GET/v2/crypto/:symbolPublic · no key · rate limited per IP

Returns one currency by ticker symbol (case-insensitive; letters, digits, - and _). /v2/crypto/currencies/:symbol is an equivalent alias. Unknown symbols return 404. Check status: a coin that has left the top-100 listing can still be returned for a while, with status: 0 and prices from its last refresh; coins that stay out of the listing are eventually removed and then return 404.

curl -s "https://api.newportexchange.org/v2/crypto/BTC"
Response 404
{"success":false,"message":"Currency not found"}

The currency object

Large integers (volume_usd_day, market_cap_usd) are returned as strings because they can exceed JavaScript’s safe integer range. Supply and percentage fields are also strings; parse them before doing arithmetic.

Currency object
{
  "id": 1,
  "unique_name": "1",
  "name": "Bitcoin",
  "alias": "bitcoin",
  "symbol": "BTC",
  "image": "btc.png",
  "rank": 1,
  "price_usd": 64250.12,
  "price_btc": "1",
  "volume_usd_day": "28456789234",
  "market_cap_usd": "1268345678901",
  "available_supply": "19741234",
  "total_supply": "19741234",
  "max_supply": "21000000",
  "percent_change_hour": "0.12",
  "percent_change_day": "1.85",
  "percent_change_week": "-2.4",
  "last_updated": "2026-09-26T13:29:00.000Z",
  "created_at": "2021-03-04T10:00:00.000Z",
  "updated_at": "2026-09-26T13:30:04.000Z",
  "status": 1,
  "sponsored": 0,
  "defi": "0"
}
Fields of a currency
FieldTypeDescription
idintegerInternal identifier.
unique_namestringCoinMarketCap’s numeric coin id, as a string.
namestringDisplay name, e.g. Bitcoin.
aliasstringURL slug, e.g. bitcoin.
symbolstringTicker, e.g. BTC.
imagestringIcon file name, e.g. btc.png. A few older rows hold an absolute URL instead.
rankintegerRank by market capitalisation.
price_usdnumberPrice in US dollars.
price_btcstring (decimal)Price in bitcoin.
volume_usd_daystring (integer)24-hour trading volume in US dollars.
market_cap_usdstring (integer)Market capitalisation in US dollars.
available_supplystring (decimal)Circulating supply.
total_supplystring (decimal) | nullTotal supply.
max_supplystring (decimal) | nullMaximum supply; null when uncapped.
percent_change_hourstring (decimal)Price change over 1 hour, in percent.
percent_change_daystring (decimal)Price change over 24 hours, in percent.
percent_change_weekstring (decimal)Price change over 7 days, in percent.
last_updatedstring (ISO 8601)When CoinMarketCap last updated this quote.
created_atstring (ISO 8601)When the coin was first recorded.
updated_atstring (ISO 8601)When Newport Exchange last refreshed the row.
statusinteger1 = in the current listing, 0 = inactive.
sponsoredinteger1 if the listing is sponsored, otherwise 0.
defistring"1" if classified as a DeFi token, otherwise "0".

Historical data

Daily OHLCV (open, high, low, close, volume) candles in US dollars for coins in the top-100 listing. Depth varies by coin: history is being backfilled towards one year per coin and is retained for up to two years, so a recently listed coin may have only a few weeks. The current day’s candle is provisional (see Data freshness). A symbol without candles returns an empty array rather than 404. These endpoints are public, rate limited per IP, and need no key.

Most recent candles

GET/v2/crypto/historical/:symbol?days=30Public · no key · rate limited per IP

Returns the most recent candles for one coin, newest first.

Query parameters for GET /v2/crypto/historical/:symbol
ParameterTypeDefaultDescription
daysinteger ≥ 1365Maximum number of daily candles to return (capped at 500). Because there is one candle per day, this is the look-back in days.
curl -s "https://api.newportexchange.org/v2/crypto/historical/BTC?days=30"

Candles by date range

GET/v2/crypto-history/:symbol?from=…&to=…Public · no key · rate limited per IP

Returns candles for one coin inside a time range, oldest first. Either days or from is required; a request with neither returns 400.

Query parameters for GET /v2/crypto-history/:symbol
ParameterTypeDefaultDescription
daysinteger ≥ 1—Range = the last N days up to now. Takes precedence over from/to.
fromunix seconds | date—Range start, e.g. 1756684800 or 2026-09-01. Required unless days is given.
tounix seconds | datenowRange end. Must not be earlier than from.
limitinteger ≥ 1500Maximum candles returned (capped at 500).
curl -s "https://api.newportexchange.org/v2/crypto-history/ETH?from=2026-09-01&to=2026-09-26"

The candle object

Candle object
{
  "id": 48211,
  "coin": "BTC",
  "time": 1758844800,
  "open": 63120.55,
  "close": 64250.12,
  "high": 64810.02,
  "low": 62890.4,
  "volume_from": 21873.6,
  "volume_to": 1398472211.8,
  "created_at": "2026-09-26T06:00:05.000Z",
  "updated_at": "2026-09-26T06:00:05.000Z"
}
Fields of a candle
FieldTypeDescription
idintegerInternal identifier.
coinstringTicker symbol, upper case.
timeinteger (unix seconds)Start of the day (00:00 UTC) the candle covers.
opennumberOpening price, USD.
highnumberHighest price, USD.
lownumberLowest price, USD.
closenumberClosing price, USD.
volume_fromnumberVolume traded, in units of the coin.
volume_tonumberVolume traded, in US dollars.
created_atstring (ISO 8601)When the candle was first stored.
updated_atstring (ISO 8601)When the candle was last refreshed.

News (public)

The public feed behind this site’s News page. It is public, rate limited per IP, and needs no key, and returns the stored record shape (unix-second timestamps, camelCase fields). For filtering by coin, source or date and a stable schema, use Posts instead.

List news articles

GET/v2/newsPublic · no key · rate limited per IP

Returns a bare array of articles, newest first, with description shortened to an excerpt of about 280 characters. Sent with Cache-Control: public, max-age=120, stale-while-revalidate=240.

Query parameters for GET /v2/news
ParameterTypeDefaultDescription
pageinteger ≥ 11Page number.
limitinteger ≥ 150Articles per page (capped at 500).
qstring ≤ 100 chars—Substring match on the title and description.
langstring—Language code, e.g. en.
curl -s -i "https://api.newportexchange.org/v2/news?q=ethereum&limit=10&page=1"
Article object (list item)
{
  "id": 9876,
  "author": "CoinDesk",
  "title": "Ethereum developers set date for next network upgrade",
  "alias": "ethereum-developers-set-date-next-network-upgrade",
  "description": "Core developers agreed on a mainnet activation date for...",
  "url": "https://www.coindesk.com/tech/2026/09/25/…",
  "urlToImage": "https://images.example.com/eth-upgrade.jpg",
  "publishedAt": 1758808980,
  "lang": "en",
  "status": 1,
  "twitter_post": 0,
  "created_at": "2026-09-25T20:00:12.000Z",
  "updated_at": "2026-09-25T20:00:12.000Z"
}
Fields of a news article
FieldTypeDescription
idintegerNumeric identifier.
authorstringAuthor or publisher; may be empty.
titlestringHeadline.
aliasstringURL slug; use it with /news/:alias.
descriptionstringExcerpt in list responses; full article text from /news/:alias.
urlstring (URL)Original article.
urlToImagestring (URL) | nullLead image.
publishedAtinteger (unix seconds)Publication time.
langstringLanguage code.
statusintegerAlways 1 (published) in responses.
twitter_postintegerInternal flag; ignore.
created_atstring (ISO 8601)When the article was ingested.
updated_atstring (ISO 8601)When the record last changed.

Get a news article

GET/v2/news/:aliasPublic · no key · rate limited per IP

Returns one article by alias or numeric id, with the full description. Unknown articles return 404 with News item not found.

curl -s "https://api.newportexchange.org/v2/news/ethereum-developers-set-date-next-network-upgrade"

Data freshness & sources

Newport Exchange ingests data on a schedule and serves it from its own database. Nothing is streamed live, and there is no WebSocket API. Poll no faster than the data changes:

Data sources and refresh schedule
DatasetTypeDescription
Market listingsCoinMarketCapTop 100 coins by market cap, refreshed every 30 minutes. Responses may also be cached for up to 30 seconds.
Historical candlesCoinMarketCap (CryptoCompare fallback)One candle per coin per UTC day. Today’s candle is built from the 30-minute listing snapshots and changes during the day; yesterday’s is replaced with the provider’s official OHLCV once a day. Older history is backfilled gradually, a few coins per day.
News and postsNewsAPI and CryptoCompare NewsIngested twice a day. Responses may be cached for 1–2 minutes.

Best practices

Keep keys server-side

  • Keep keys in environment variables or a secrets manager, never in source control.
  • Call authenticated endpoints only from your server. If a browser or mobile app needs posts, proxy them through your backend and cache the result.
  • Use one key per application or environment so each can be revoked alone.
  • Browser access from third-party origins is not guaranteed for any endpoint (CORS is restricted), which is one more reason to call the API server-side.

Caching

Respect Cache-Control: posts can be reused for 60 seconds, market data for 30 seconds and public news for 2 minutes. Since listings refresh every 30 minutes and news twice a day, caching responses for several minutes in your own layer costs you nothing in freshness and keeps you well inside the rate limits. Treat post id and slug as stable keys for your own storage.

Retries and backoff

  • Retry only 429, 5xx and network errors. Never retry 400, 401 or 404 unchanged — the answer will not change.
  • On 429, wait at least RateLimit-Reset seconds before the next request.
  • Otherwise back off exponentially (1 s, 2 s, 4 s, … capped around 60 s) and add random jitter so many clients do not retry in lockstep. Give up after a few attempts and alert.
  • Watch RateLimit-Remaining and slow down before it reaches zero.

The polling example below includes a complete retry helper.

Polling for new posts

To keep a local copy of posts up to date, store the published_at of the newest post you have processed and ask only for posts from that moment on, oldest first. This makes polling idempotent: running it twice fetches nothing new, and a crash between fetching and saving just replays a few posts.

  • Use from with sort=oldest, so posts that arrive while you page are appended at the end instead of shifting earlier pages.
  • from is inclusive, so de-duplicate by id the posts published in the boundary second.
  • Save the cursor only after the posts have been handled. Poll every few minutes at most — news is ingested twice a day.
# One polling step: everything published at or after a stored cursor
# (unix seconds), oldest first, so pages stay stable while new posts arrive.
SINCE=1758808980   # published_at of the newest post you have already processed

curl -s -G "https://api.newportexchange.org/v2/posts" \
  -H "X-API-Key: YOUR_API_KEY" \
  --data-urlencode "from=$SINCE" \
  --data-urlencode "sort=oldest" \
  --data-urlencode "limit=100" \
  --data-urlencode "page=1"
# Repeat with page=2, 3, … while meta.has_more is true.
# Skip ids you have already processed: `from` is inclusive of the boundary.

Handling has_more

Loop while meta.has_more is true, incrementing page and keeping every other parameter identical. Use limit=100 for bulk syncs to make the fewest requests. Do not infer the end from a short page or compute page counts — there is no total. For very large backfills, split the work into from/to windows (for example one per day) rather than paging hundreds deep.

Changelog

    • New key-authenticated GET /v2/posts and GET /v2/posts/:slug with search, coin, source and date filters.
    • API keys (X-API-Key) with per-key rate limits and standard RateLimit-* headers.
    • First public reference for the /v2 market-data, historical and news endpoints.

Support

For API keys, higher rate limits, bug reports or questions, email [email protected]. When reporting a problem, include the request URL (without your key), the time in UTC, the HTTP status and the response body. Never send us your API key — tell us the name or email it was issued under instead.