FISL is an HTTP lookup API that maps CIDs to locations, so that a client with a CID can find out where to retrieve it from and who announced it. A client sends a CID, and the index returns the URLs where content can be retrieved, along with the identity of the publisher when known. FISL specifies only the query interface, so it works with any indexing system that supplies location records.
Content-addressing decouples two questions that are usually tangled together: what the data is, and where you get it. A client that has a CID can verify the bytes it receives, no matter who serves them ([[ipfs-principles]]). This leaves one problem open: given a CID, where do you fetch it from?
RASL ([[rasl]]) answers this with inline hints, but hints are static: they are baked into the URL at the time of writing and go stale. FISL makes discovery dynamic: a client asks an indexer at the moment of retrieval, and the answer reflects what publishers currently announce.
FISL specifies only the query interface. How an indexer collects, validates, and expires location records is out of scope, so any indexing system that supplies location records can sit behind FISL. ATFISL ([[atfisl]]), a publication and indexing layer on the AT Protocol, is one such system.
A FISL record states one set of retrieval locations for the requested CID, and who announced them, if the indexer knows. Its fields are:
publisher (optional): the identity of the publisher, as a string that matches
the DID syntax of [[did-core]]. Any DID method is allowed, for example
did:key ([[did-key]]), did:plc ([[did-plc]]), or
did:web ([[did-web]]). Indexers and clients must not reject a
publisher because they do not know its method, and a client does not have to
resolve it: FISL uses the publisher as an opaque identity. For an index built
with ATFISL, this is the DID of the repository that published the record
([[atfisl]]). An indexer omits publisher when it has no publisher identity
for the record. Clients must not treat a record as more trustworthy because it carries a
publisher: only CID verification proves the bytes.
addrs (required): an array of one or more addr objects, each describing one
location at which the content can be retrieved.
size (optional): the size of the content in bytes, as an integer. This is an
unverified hint for clients to plan retrieval.
expires (optional): the time after which this record is no longer valid, as a
datetime.
Each addr object has the fields:
url (required): an absolute URL ([[url]]) from which the content can be
retrieved. The scheme identifies the retrieval method, and with it how a client turns the
URL and the CID into a request (see Transport
agnosticity). This specification defines retrieval for https (see
Retrieval). Clients must skip URLs whose schemes they do not
support.
expires (optional): the time after which this addr is no longer valid, as a
datetime.
The record's expires is a ceiling: the effective expiry of an addr is the
earliest of its own expires and the record's, whichever of the two are present.
An addr without its own expires inherits the record's. An addr with neither has
no stated expiry. An absent expires states no expiry; it does not mean that the
record or the addr is expired.
Indexers should not return a record after its expires, and should not return an
addr after its effective expiry. Clients should check these times and ignore records and
addrs that are expired. A record with no stated expiry is retired only by the indexer's own
freshness policy, which is out of scope of this specification. expires is a
publisher's claim about validity, not the mechanism that keeps the index live.
A datetime is a string that should meet the intersecting requirements of the RFC 3339, ISO 8601, and WHATWG HTML datetime standards, as specified for the AT Protocol Lexicon datetime type ([[lexicon]]).
Entries and addr objects may carry additional fields. Clients must ignore fields they do not recognize.
An indexer answers lookups at:
GET /routing/locations/{cid}/{format}
{cid} is the DASL or BDASL CID to look up in string form ([[cid]],
[[bdasl]]). Because DASL CIDs have exactly one valid string encoding, no normalization is
needed and the path is maximally HTTP-cacheable.
{format} is the response format: json or jsonl.
Responses:
200: the body contains zero or more locations. A CID with no live records is
not an error.
308: the request path has no {format} segment. An indexer may
redirect /routing/locations/{cid} to
/routing/locations/{cid}/json. An indexer that does not redirect responds
with 404. Clients must always request an explicit format and must not rely on
this redirect.
400: {cid} is not a valid DASL or BDASL CID ([[bdasl]]).
404: {format} is not a format this indexer supports.
The format is named by the path, not negotiated. An indexer must support both formats, and
states the format of the body in the Content-Type response header:
json (application/json): a JSON object with a
locations array.
jsonl (application/jsonl): JSON Lines ([[jsonl]]) one
location record per line, each a JSON object followed by a newline, with no wrapper
object.
Indexers must not select the format with the Accept request header. A format
added by a later revision of this specification takes its own path segment, and an indexer
that does not implement it responds with 404.
The json body is a JSON object. A CID with no live records returns
{"locations": []}.
{
"locations": [
{
"publisher": "did:plc:ewvi7nxzyoun6zhxrhs64oiz",
"addrs": [
{
"url": "https://berjon.com/.well-known/rasl/bafkreifn5yxi7nkftsn46b6x26grda57ict7md2xuvfbsgkiahe2e7vnq4"
},
{ "url": "https://mirror.example.com/kitten.jpg", "expires": "2026-09-01T00:00:00Z" }
],
"size": 94201,
"expires": "2026-09-18T00:00:00Z"
}
]
}
The JSONL body contains one location record per line. A CID with no live records returns
an empty body with status 200.
{"publisher":"did:plc:ewvi7nxzyoun6zhxrhs64oiz","addrs":[{"url":"https://berjon.com/.well-known/rasl/bafkreifn5yxi7nkftsn46b6x26grda57ict7md2xuvfbsgkiahe2e7vnq4"}],"size":94201,"expires":"2026-09-18T00:00:00Z"}
{"publisher":"did:web:mirror.example.com","addrs":[{"url":"https://mirror.example.com/kitten.jpg","expires":"2026-08-30T00:00:00Z"}],"expires":"2026-09-01T00:00:00Z"}
{"addrs":[{"url":"https://cache.example.net/kitten.jpg"}]}
JSONL exists for streaming. An indexer can write each record as it finds it, for example when the index is sharded across backends, or when entries are relayed from a live subscription, instead of buffering the full set. Clients must parse lines incrementally and must not assume a bound on the number of entries.
Responses should carry a Cache-Control: public, max-age={ttl} header, where
ttl does not exceed the time until the earliest expiry in the
response. When no record in the response states an expiry, the indexer selects
ttl itself and should keep it short.
Indexers may also send stale-while-revalidate and
stale-if-error ([[rfc5861]]). Both headers let a cache serve a stored response
after its max-age ends. The full time in which a cache can answer is therefore
max-age plus that value, and that total must not exceed the time until the
earliest expiry:
max-age + stale-while-revalidate <= time to earliest expiry
If the total exceeds it, a cache answers with records that the client then ignores as expired. The lookup succeeds, but it gives the client no location to try.
Within that bound, a stale answer costs the client little: it makes one failed attempt and
then tries another addr. These extensions therefore let an indexer keep
max-age short and still answer most requests from cache.
Empty results should use a short max-age (e.g. 5 seconds) and no
stale-while-revalidate. A stale empty response leaves a client with no
location to try at all.
Indexers must support CORS so browser clients on any origin can query them:
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, OPTIONS
FISL only does discovery. To retrieve, a client picks the URLs it supports, fetches them in whatever order it prefers, and verifies that the received bytes hash to the requested CID; on mismatch discarding the response and trying another location.
For an https URL, retrieval follows steps 2 through 8 of the steps to
fetch a RASL URL ([[rasl]]),
with the URL used as the request URL directly instead of constructing one from a hint: a
stateless GET with no content negotiation, redirects treated as 307, the
response treated as application/octet-stream, and the bytes verified against
the CID. A RASL server therefore needs no special marking in a location entry — its
/.well-known/rasl/{cid} URL is announced as an ordinary https URL.
For BDASL CIDs, clients may use streaming verification ([[bdasl]]) instead of whole-content hashing.
An addr is a URL and nothing more. The scheme names the transport, so the transport is encapsulated by the URL and never appears as a separate field. A new transport needs a scheme and a specification of that scheme. It does not need a new field, a registry at the indexer, or a new version of this API.
Indexers treat URLs opaquely. An indexer must not drop, rewrite, or reject an addr because it does not know the scheme; it returns what publishers announce. Selection is the client's job: a client uses the schemes it supports and skips the rest.
A scheme specification states how a client gets the bytes for a CID from the URL. Two shapes are common:
https://mirror.example.com/kitten.jpg is used as-is (see
Retrieval).
radiroh://<endpoint-id> ([[radiroh]]) names an iroh endpoint (a 32-byte
public key) which is resolved to a relay address used to establish a direct connection,
over which the client fetches over iroh-blobs ([[iroh-blobs]]). The CID is absent from the
URL because the client already holds it: it is the CID it looked up. One such URL therefore
covers every CID that endpoint serves.
A URL does not have to carry network coordinates. radiroh:// names an endpoint
identity only and leaves resolution to the client. A scheme that named a RASL server instead
of a byte location would fit the same model: the client would build the
/.well-known/rasl/{cid} URL from the CID it looked up ([[rasl]]).
Whatever the scheme, the client checks that the bytes hash to the requested CID. Some
transports verify as they stream: iroh-blobs with BLAKE3 ([[blake3]]), or BDASL over
https ([[bdasl]]).
An entry may mix transports. A client picks whichever addr it can use:
{
"publisher": "did:plc:ewvi7nxzyoun6zhxrhs64oiz",
"addrs": [
{
"url": "https://berjon.com/.well-known/rasl/bafkreifn5yxi7nkftsn46b6x26grda57ict7md2xuvfbsgkiahe2e7vnq4"
},
{ "url": "radiroh://25igmk4u75iqsbosfvep5sxoozazizgoxfnuqzsnl22pbddig5ra" }
],
"expires": "2026-09-18T00:00:00Z"
}
publisher, or by the URL origin
when the record has no publisher.
http URLs unless explicitly configured otherwise, for
testing and private networks.
size is unverified. Clients must not allocate resources based on it without
bounds.
This section is informational. The lookup API path and shape are modelled on the IPFS
Delegated Routing V1 HTTP API ([[routing-v1]]) and its generic schema
([[ipip-0518]]), which motivated this design. FISL is not conformant with that API and does
not intend to be: it renames the providers path segment and response key to
locations and the ID field to publisher, it drops the
Schema and Protocols fields,
multiaddr support, and libp2p peer records, it names the response format in the path instead
of negotiating it with Accept, and it adds optional expiry.