URL lookup
GET /v1/lookup?url= takes the public URL of a post, video, repository, package, article or profile and answers exactly as the matching endpoint would. You do not have to know which platform endpoint to call or which parameters it takes: paste the link, and the response tells you what it resolved to.
GET https://api.onekey.dev/v1/lookup?url={public URL}
x-api-key: YOUR_API_KEYWhen to use it#
- Your input is a link a user pasted, a URL from a feed, or a bookmark, and you want the record behind it.
- You handle many platforms and would rather not keep a table of URL formats and endpoint parameters in your own code.
- You still get every guarantee of the endpoint it resolves to: the same response format, the same price, the same caching, the same refunds.
Resolution is plain string matching against URL shapes declared on the endpoints themselves (the table below). Nothing is guessed and the URL is never fetched: only the platform's own data source is.
The request#
curl "https://api.onekey.dev/v1/lookup?url=https://github.com/facebook/react" \
-H "x-api-key: YOUR_API_KEY"| Parameter | Required | What it is |
|---|---|---|
url | yes | An absolute http or https URL, at most 2048 characters. Encode it as you would any query value. |
cursor | no | Passed through to the resolved endpoint when it pages (see Pagination). Today none of the endpoints reached by URL page, so you will not need it yet. |
The response#
The body is the resolved endpoint's normal response plus one extra top-level key, resolved. This is github.com/facebook/react as recorded from the live API:
{
"success": true,
"data": {
"entity": {
"id": "10270250",
"name": "react/react",
"description": "The library for web and native user interfaces.",
"url": "https://github.com/react/react",
"stats": { "stars": 250835, "forks": 51416, "openIssues": 1384, "watchers": 6601 },
"language": "JavaScript",
"createdAt": "2013-05-24T16:15:54Z",
"updatedAt": "2026-09-30T03:07:37Z",
"tags": ["declarative", "frontend", "javascript", "library", "react", "ui"]
},
"package": { "pushedAt": "2026-09-29T22:40:19Z", "license": "MIT", "defaultBranch": "main", "archived": false, "homepageUrl": "https://react.dev" },
"author": { "username": "react", "avatarUrl": "https://avatars.githubusercontent.com/u/102812?v=4" }
},
"metadata": {
"platform": "github",
"endpoint": "repo",
"cached": false,
"stale": false,
"age_s": 0.015,
"fetched_at": "2026-09-30T03:40:59.096767+00:00",
"credits_charged": 1,
"request_id": "bcecb897f94848c1b5dd5cc82f68b0f4",
"idempotent_replay": false
},
"error": null,
"resolved": { "platform": "github", "endpoint": "repo", "params": { "owner": "facebook", "repo": "react" } }
}| Field | Meaning |
|---|---|
resolved.platform, resolved.endpoint | The endpoint that answered. metadata.platform and metadata.endpoint say the same. |
resolved.params | The parameters extracted from the URL, after the endpoint's own normalisation. Calling GET /v1/{platform}/{endpoint} with exactly these gives the same answer. |
data, metadata, pagination | As on the resolved endpoint's page, including its computed block where it has one. |
A URL that resolves to a list endpoint answers with that endpoint's list and, if it pages, its pagination block.
What resolves#
65 URL shapes lead to 54 endpoints on 29 platforms. This table is generated from the same declarations the API matches against, so it is always current; the live list is also x-resolves-to on /v1/lookup in /openapi.json, and features.lookup in GET /v1/platforms marks each endpoint that has a shape.
| URL shape | Resolves to | Example |
|---|---|---|
bsky.app/profile/{actor}/post/{rkey} | bluesky.post_thread | https://bsky.app/profile/did:plc:z72i7hdynmk6r22z27h6tvur/post/3mw2cdr44fc2a |
bsky.app/profile/{handle} | bluesky.profile | https://bsky.app/profile/bsky.app |
crates.io/crates/{name} | crates.crate | https://crates.io/crates/serde |
dev.to/{username}/{slug} | devto.article_by_path | https://dev.to/devteam/changelog-query-the-dev-api-from-a-javascript-frontend-329j |
dev.to/{username} | devto.profile | https://dev.to/ben |
hub.docker.com/r/{namespace}/{repo} | dockerhub.repo | https://hub.docker.com/r/library/nginx |
gist.github.com/*/{id} | github.gist | https://gist.github.com/…/6efdf9ca47239507e6a47a208580dc68 |
gist.github.com/{id} | github.gist | https://gist.github.com/6efdf9ca47239507e6a47a208580dc68 |
github.com/{owner}/{repo}/issues/{number} | github.issue | https://github.com/facebook/react/issues/1 |
github.com/orgs/{org} | github.org | https://github.com/orgs/github |
github.com/{username} | github.profile | https://github.com/torvalds |
github.com/{owner}/{repo}/pull/{number} | github.pull | https://github.com/facebook/react/pull/1 |
github.com/{owner}/{repo} | github.repo | https://github.com/facebook/react |
gitlab.com/{project+}/-/issues/{iid} | gitlab.issue | https://gitlab.com/gitlab-org/gitlab/-/issues/1 |
gitlab.com/{project+}/-/merge_requests/{iid} | gitlab.merge_request | https://gitlab.com/gitlab-org/gitlab/-/merge_requests/1 |
gitlab.com/{project+} | gitlab.project | https://gitlab.com/gitlab-org/gitlab |
gitlab.com/{username} | gitlab.user | https://gitlab.com/gitlab-bot |
news.ycombinator.com/item?id={id} | hackernews.item | https://news.ycombinator.com/item?id=8863 |
news.ycombinator.com/user?id={username} | hackernews.user | https://news.ycombinator.com/user?id=pg |
huggingface.co/{owner}/{name} | huggingface.model | https://huggingface.co/google-bert/bert-base-uncased |
huggingface.co/{name} | huggingface.user | https://huggingface.co/julien-c |
instagram.com/p/{shortcode} | instagram.post | https://instagram.com/p/CUbHfhpswxt |
instagram.com/reel/{shortcode} | instagram.post | https://instagram.com/reel/CUbHfhpswxt |
instagram.com/{handle} | instagram.profile | https://instagram.com/instagram |
komi.io/{handle} | komi.page | https://komi.io/komi |
lnk.bio/{handle} | linkbio.page | https://lnk.bio/linkbio |
link.me/{handle} | linkme.page | https://link.me/linkme |
linktr.ee/{handle} | linktree.page | https://linktr.ee/linktree |
lobste.rs/s/{id}/** | lobsters.story | https://lobste.rs/s/q2rymh |
lobste.rs/~{username} | lobsters.user | https://lobste.rs/~jcs |
blog.naver.com/{handle} | naver.blog | https://blog.naver.com/blogpeople |
npmjs.com/package/{name} | npm.package | https://npmjs.com/package/left-pad |
npmjs.com/package/{name+} | npm.package | - |
openlibrary.org/works/{id}/** | openlibrary.work | https://openlibrary.org/works/OL45804W |
producthunt.com/posts/{slug} | producthunt.post | https://producthunt.com/posts/example |
pypi.org/project/{name} | pypi.package | https://pypi.org/project/requests |
reddit.com/r/*/comments/{id}/** | reddit.post | https://reddit.com/r/…/comments/abc121 |
redd.it/{id} | reddit.post | https://redd.it/abc121 |
reddit.com/r/{subreddit} | reddit.subreddit_about | https://reddit.com/r/python |
reddit.com/user/{username} | reddit.user_about | https://reddit.com/user/spez |
reddit.com/u/{username} | reddit.user_about | https://reddit.com/u/spez |
rubygems.org/gems/{name} | rubygems.gem | https://rubygems.org/gems/rails |
soundcloud.com/{user} | soundcloud.profile | https://soundcloud.com/forss |
soundcloud.com/{user}/{track} | soundcloud.track | https://soundcloud.com/forss/flickermood |
open.spotify.com/album/{id} | spotify.album | https://open.spotify.com/album/6dVIqQ8qmQ5GBnJ9shOYGE |
open.spotify.com/artist/{id} | spotify.artist | https://open.spotify.com/artist/4Z8W4fKeB5YxbusRsdQVPb |
open.spotify.com/track/{id} | spotify.track | https://open.spotify.com/track/4uLU6hMCjMI75M1A2tKUQC |
stackoverflow.com/questions/{id}/** | stackexchange.question | https://stackoverflow.com/questions/11227809 |
stackoverflow.com/q/{id} | stackexchange.question | https://stackoverflow.com/q/11227809 |
stackoverflow.com/users/{id}/** | stackexchange.user | https://stackoverflow.com/users/22656 |
tiktok.com/@{handle} | tiktok.profile | https://tiktok.com/@tiktok |
tiktok.com/*/video/{id} | tiktok.video | https://tiktok.com/…/video/6718335390845095173 |
twitch.tv/{login} | twitch.profile | https://twitch.tv/shroud |
twitch.tv/videos/{id} | twitch.video | https://twitch.tv/videos/2886470967 |
en.wikipedia.org/wiki/{title} | wikipedia.article | https://en.wikipedia.org/wiki/Alan%20Turing |
x.com/*/status/{id} | x.tweet | https://x.com/…/status/1519781379172495360 |
twitter.com/*/status/{id} | x.tweet | https://twitter.com/…/status/1519781379172495360 |
x.com/{handle} | x.user | https://x.com/jack |
twitter.com/{handle} | x.user | https://twitter.com/jack |
youtube.com/channel/{id} | youtube.channel | https://youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw |
youtube.com/@{handle} | youtube.channel_by_handle | https://youtube.com/@rickastley |
youtube.com/watch?v={id} | youtube.video | https://youtube.com/watch?v=dQw4w9WgXcQ |
youtu.be/{id} | youtube.video | https://youtu.be/dQw4w9WgXcQ |
youtube.com/shorts/{id} | youtube.video | https://youtube.com/shorts/dQw4w9WgXcQ |
youtube.com/embed/{id} | youtube.video | https://youtube.com/embed/dQw4w9WgXcQ |
In a URL shape, {name} is one path segment captured as that parameter, {name+} is two or more segments, … is one segment whose value is ignored, and a shape ending in /** ignores anything after it. Examples are built from each endpoint's own example parameters.
Platforms whose host is chosen by the user, such as Mastodon and Lemmy instances, have no shape: there is no fixed host to match.
How a URL is matched#
- The scheme must be
httporhttps. Which one does not matter. - The host is lower-cased and a leading
www.,m.ormobile.is dropped, sohttps://m.github.com/torvalds/linuxresolves likegithub.com/torvalds/linux. - A trailing
/, repeated slashes and the#fragmentare ignored. Query parameters the shape does not name are ignored (?utm_source=...is harmless); the ones it names, such asidinnews.ycombinator.com/item?id={id}, must be present. - When two shapes on one host could both match, the one with more fixed segments wins:
github.com/orgs/{org}beatsgithub.com/{owner}/{repo}forgithub.com/orgs/facebook. Shapes that could tie are refused when the catalog is built, so a URL never resolves to two endpoints. - The captured value must satisfy the endpoint's own parameter rules (the Constraints column on its page).
github.com/orgs/Bad_Org!matches the shape but not the parameter, so it does not resolve.
Anything else is 422 UNSUPPORTED_URL. The message never repeats your URL.
Edge cases#
- The URL is not fetched. Only the platform's data source is. A link that exists but points at a private or deleted record answers the way the endpoint does:
404 NOT_FOUND, free. - One-segment GitLab paths resolve to the user endpoint.
gitlab.com/<name>is ambiguous between a user and a group; it resolves togitlab.user, and a group at that path answers404 NOT_FOUND(refunded). - A URL can resolve and still fail. The lookup only picks the endpoint; the fetch can fail like any other call (
502, refunded). - Cache hits are free here too. Looking up
news.ycombinator.com/item?id=8863right after callinghackernews.itemwithid=8863is a cache hit (X-Cache: HIT,credits_charged: 0): both build the same parameters. - Idempotency works as on a direct call. Send an
Idempotency-Key; it is bound to the lookup query string, so the same URL with the same key is replayed for free for 24 hours. A lookup and a direct endpoint call are different requests for that purpose even when they resolve to the same parameters. - Different URLs for the same record are cache-equivalent when they resolve to the same parameters:
youtu.be/{id},youtube.com/watch?v={id}andyoutube.com/shorts/{id}all becomeyoutube.videowith the sameid.
Unsupported URLs#
curl "https://api.onekey.dev/v1/lookup?url=https://example.com/whatever" \
-H "x-api-key: YOUR_API_KEY"
# HTTP/1.1 422 Unprocessable Content{
"success": false,
"data": null,
"metadata": { "request_id": "bb8c4300f1fb4017b9e2eeaa4e0681bf" },
"error": {
"code": "unsupported_url",
"type": "UNSUPPORTED_URL",
"message": "No endpoint matches this URL",
"request_id": "bb8c4300f1fb4017b9e2eeaa4e0681bf",
"doc_url": "https://onekey.dev/errors/UNSUPPORTED_URL"
}
}A value that is not an absolute http(s) URL at all (not-a-url, or over 2048 characters) is the same 422 UNSUPPORTED_URL with the message url must be an absolute http(s) URL. Leaving url out is 422 INVALID_PARAMS. None of these are charged.
What it costs#
Exactly the resolved endpoint's price, shown on its page: endpoints cost 1 to 8 credits. There is no surcharge for the lookup itself. A cache hit is free (a cached response over 100 KB costs 0.2 credit), a not found or a source failure is refunded, and an unsupported URL is rejected before any charge. metadata.credits_charged tells you what this call cost.
Errors it can return#
| Status | Type | When |
|---|---|---|
| 422 | UNSUPPORTED_URL | No URL shape matches, or the value is not an absolute http(s) URL. Free. |
| 422 | INVALID_PARAMS | url is missing. Free. |
| 404 | NOT_FOUND | The URL resolved, but the record does not exist on the platform. Free. |
| 401 | UNAUTHORIZED | Bad or missing key. |
| 402 | INSUFFICIENT_CREDITS, KEY_BUDGET_EXCEEDED | The resolved endpoint's price cannot be covered. Nothing charged. |
| 429 | RATE_LIMITED | Wait Retry-After seconds. |
| 502 | UPSTREAM_ERROR and the other UPSTREAM_* types | The platform failed. Refunded; retry with backoff. |
Plus the idempotency answers when you send an Idempotency-Key. See Errors for the full list.
Try it: URL to data · URL to endpoint finder