REST API
Arcscan reads Arc through an HTTP API, and that API is reachable from outside with no key and no account — every path on this page was called over the public internet before it was written down.
Base URL#
The API has hostnames of its own, one per chain: api.arc-scan.io for Arc mainnet, chain 5042, and api-testnet.arc-scan.io for Arc Testnet, chain 5042002. These are the addresses to build against:
https://api.arc-scan.io/v1 # Arc mainnet, chain 5042 https://api-testnet.arc-scan.io/v1 # Arc Testnet, chain 5042002
Each host serves the same two surfaces: /v1/…, the typed REST API — one path per resource, returning the documents this explorer itself renders from — and /api?module=…&action=…, the explorer-compatible contract that existing tooling already speaks. The two hosts differ only in the chain they answer for, so moving a client to testnet is a change of host and nothing else.
The site’s own /_api path is not the API
You may notice that the explorer fetches /_api/v1/… from its own origin, and that the path answers if you call it yourself. It is the website’s internal rewrite to this same service, kept for the site’s own pages: it sends no CORS headers, it is served with a noindex header, and it can be changed for a reason that has nothing to do with you. Build against the hostnames above, and pin the host in one constant so you can change it in one place.Callable from a browser — on these hosts only
The API hosts answer cross-origin:Access-Control-Allow-Origin: *, with Content-Type, X-Api-Key and If-None-Match allowed on a preflight, and ETag, X-Request-Id and the rate-limit headers exposed to the caller. The explorer’s own /_api path carries no Access-Control- header at all, so a fetch() aimed there fails in a browser while the same call succeeds in curl — which is exactly how the wrong address goes unnoticed.Two surfaces#
The same node and the same index answer through two different shapes. The typed API is what this explorer renders from, so anything visible on a page can be read as JSON in the same form. The compatible API reproduces the de-facto explorer REST contract that wallets, deploy scripts and indexers already speak, so existing tooling can be pointed at Arc by changing one base URL.
| Surface | Shape | Success envelope |
|---|---|---|
/v1/… | Typed REST, one path per resource | The document itself, snake_case keys |
/api?module=…&action=… | One path, dispatched on query string | {"status":"1","message":"OK","result":…} |
Endpoints#
Every path below was called against mainnet when this page was written. All of them answered; two answered 501, and both are named where they belong rather than left out — a documented refusal is worth more than a gap.
Chain, home and head#
/v1/chain is the first call to make: it states the chain id, the native currency and its decimals, which capabilities this deployment has, and exactly how much of the chain is indexed. Read it rather than hard-coding any of that — the two chains differ, and Data coverage explains what the index block tells you.
curl https://api.arc-scan.io/v1/chain # {"chain_id":5042,"name":"Arc","is_testnet":false, # "native":{"symbol":"USDC","decimals":18}, # "block_time_ms":506,"finality":"instant", # "capabilities":{"trace":true,"archive":true,"debug":false,…}, # "index":{"available":true,"complete":true,…}}
| Path | What it answers |
|---|---|
GET /v1/chain | Chain constants, capabilities and index coverage |
GET /v1/home | Latest blocks and transactions in one document |
GET /v1/stats/summary | The headline counters the home page shows |
GET /v1/stats/gas | Fee tracker: current and recent gas prices |
GET /v1/stream/head | Server-sent events, one frame per new block |
/v1/stream/head is an SSE stream, not a JSON document — it stays open and pushes a frame roughly twice a second. Every frame carries server_now so a client can compute an honest age without trusting its own clock.
curl -N https://api.arc-scan.io/v1/stream/head # event: head # data: {"height": 14852424, "hash": "0x68ce06…f3775", "timestamp": 1786360005, # "tx_count": 0, "server_now": 1786360007, "available": true}
Blocks#
A block reference is a height, a hash, or the literal latest. Lists take limit and an opaque cursor; see Blocks for what the fields mean.
curl "https://api.arc-scan.io/v1/blocks?limit=2" curl https://api.arc-scan.io/v1/blocks/latest curl https://api.arc-scan.io/v1/blocks/14852000 curl "https://api.arc-scan.io/v1/blocks/14852000/txs?limit=10"
Transactions#
A transaction is addressed by hash. The call tree is available on mainnet because trace is true there; the state diff is not, because debug is false, and it says so with a 501 rather than an empty answer.
H=0x128cb07da24289341bcc57e3129ef78f82b5a1769c30156774487fe9e98251e9 curl "https://api.arc-scan.io/v1/txs?limit=5" curl "https://api.arc-scan.io/v1/txs/$H" curl "https://api.arc-scan.io/v1/txs/$H/raw" curl "https://api.arc-scan.io/v1/txs/$H/trace" curl "https://api.arc-scan.io/v1/txs/$H/state" # 501 {"error":{"code":"CAPABILITY_UNAVAILABLE",…}} — no debug namespace on mainnet
Addresses and tokens#
A=0xf40dc55b06e4e8c042430ecf1995d0ec6b9ae033 T=0x3600000000000000000000000000000000000000 # the native currency as an ERC-20 curl "https://api.arc-scan.io/v1/address/$A" curl "https://api.arc-scan.io/v1/address/$A/txs?limit=10" curl "https://api.arc-scan.io/v1/address/$A/activity?limit=10" curl "https://api.arc-scan.io/v1/address/$A/logs?limit=10" curl "https://api.arc-scan.io/v1/address/$A/tokens" curl "https://api.arc-scan.io/v1/tokens/$T" curl "https://api.arc-scan.io/v1/tokens/$T/info"
Holders are indexed on mainnet, so /v1/tokens/{address}/holders answers for ordinary tokens. It refuses for the native currency at 0x3600…0000, and the message explains why: those balances are account balances, and indexing them twice would double-count every account.
Search, charts and decode#
Search resolves what the explorer’s own box resolves — a height, a hash, an address. The charts index lists every metric with its id, and /v1/charts/{metric} returns that one series; tx is one of them. Decode is the only POST on this page and needs no chain lookup at all.
curl "https://api.arc-scan.io/v1/search?q=0x3600000000000000000000000000000000000000" curl "https://api.arc-scan.io/v1/search/suggest?q=0x36" curl https://api.arc-scan.io/v1/charts curl https://api.arc-scan.io/v1/charts/tx curl -X POST https://api.arc-scan.io/v1/decode \ -H 'content-type: application/json' \ -d '{"input":"0xa9059cbb…"}' # {"selector":"0xa9059cbb","decoded":{"name":"transfer", # "signature":"transfer(address,uint256)","args":[…]},"error":null}
The compatible surface#
/api?module=…&action=… speaks the familiar explorer contract: one path, a module and an action, and every scalar in result as a string. The proxy module passes JSON-RPC reads through and answers in JSON-RPC shape rather than the status envelope.
curl "https://api.arc-scan.io/api?module=proxy&action=eth_blockNumber" # {"jsonrpc":"2.0","id":1,"result":"0xe2a12f"} curl "https://api.arc-scan.io/api?module=account&action=balance&address=0xf40dc55b06e4e8c042430ecf1995d0ec6b9ae033" # {"status":"1","message":"OK","result":"20456053552414099311"} curl "https://api.arc-scan.io/api?module=stats&action=ethsupply"
Not every action of that contract exists here — the ones that need data Arc does not have, or an index we do not keep, are refused by name. The on-site API referenceOpens in a new tab lists the full dispatch table and which actions are unavailable; it is generated from the service itself, so it cannot drift the way a page like this one can.
Reading the JSON#
Four conventions cause every integration bug worth naming. The first is the expensive one: an 18-decimal amount does not survive a JavaScript Number, so amounts cross the wire as strings and must be parsed as such.
| Convention | What it means on the wire |
|---|---|
| Amounts | An object, not a number: raw (integer string), decimals, formatted (exact), usd, symbol. Gas figures and supplies are decimal strings too. Block heights are JSON numbers. |
| Casing | Keys are snake_case. Addresses and hashes are lowercase, with the display form beside them in a checksum field. Any input case is accepted. |
| Time | Unix seconds, never pre-formatted. Blocks arrive about twice a second and timestamps have one-second resolution, so they are not a total order — never sort or paginate by them. |
| Pagination | Opaque cursors under page.next, not page numbers. page.has_more tells you whether to keep going. |
Committed history is immutable, and cached like it
A response keyed by a committed height or a mined hash is servedCache-Control: public, max-age=31536000, immutable. Arc has instant finality and no reorganisations, so that is a real guarantee rather than a probabilistic one: cache those responses forever and skip the request entirely.Errors#
A refusal is a JSON object with a machine-readable code, a message written for a human, and an optional detail. Branch on code, never on the message text.
curl https://api.arc-scan.io/v1/blocks/999999999999 # 404 # {"error":{"code":"NOT_FOUND","message":"No block at height 999999999999","detail":null}} curl https://api.arc-scan.io/v1/tokens/0x3600000000000000000000000000000000000000/holders # 501 # {"error":{"code":"CAPABILITY_UNAVAILABLE", # "message":"The native gas token has no separate holder list: its balances are account # balances, and indexing them again would double-count every account.", # "detail":{"capability":"holder_index","detail_key":"holdersNativeToken"}}}
| Status | code | When |
|---|---|---|
| 404 | NOT_FOUND | No such block, transaction, address record or token. |
| 429 | RATE_LIMITED | Too many requests. Carries Retry-After in seconds. |
| 501 | CAPABILITY_UNAVAILABLE | The data needs a capability this chain or this deployment does not have. |
| 502 | UPSTREAM_ERROR | The node answered, but not usefully. |
| 503 | INDEX_OVERLOADED | A heavy query was shed rather than queued. |
| 504 | UPSTREAM_TIMEOUT | The node did not answer in time. |
Read the error envelope, not just the status
A malformed or out-of-range parameter — an unparseable block reference, alimit above the maximum, a missing required query, a mistyped JSON field — is answered with 400 {"error":{"code":"INVALID_INPUT","message":"limit: Input should be greater than or equal to 1","detail":null}}. The code classifies the mistake and the message names the field that caused it, which is usually faster than re-reading the URL. A 404 here means the resource genuinely does not exist.Limits and access#
There is no API key, no tier and nothing to sign up for. Requests are metered per caller by a token bucket, and every response tells you where you stand — read the headers instead of hard-coding a number, because the budget is an operational setting and not a published promise.
| Header | Meaning |
|---|---|
X-RateLimit-Limit | The size of the bucket the request was metered against. |
X-RateLimit-Remaining | Tokens left in it. |
Retry-After | On a 429 or a 503 only: whole seconds to wait before retrying. |
X-Request-Id | Present on API responses. Quote it if you need to ask about a request. |
Two other ceilings are real and are reported rather than hidden. Log queries are clamped to a maximum block span, and an over-range request is refused with that maximum named in the message instead of being silently truncated. Trace and state-diff calls run on a small dedicated pool with a wall-clock budget and a size cap, and set truncated: true rather than hanging.
Reading Arc without our index
If what you want is the chain rather than our view of it, we also run a public read-only JSON-RPC endpoint for mainnet — see Public RPC. There is also a plain-text /llms.txtOpens in a new tab describing what this explorer holds, for anything that reads prose before it reads a schema.