Docs
View as Markdown

Multi-platform search & creator card

Two endpoints send one input to several platforms at once and give you the answers side by side. GET /v1/search/multi runs the same query on each platform's search endpoint; GET /v1/creator/card fetches the same handle's profile on each. Each platform is a separate leg that runs through the ordinary pipeline, so prices, caching and refunds are exactly those of the endpoint it uses.

shell
GET https://api.onekey.dev/v1/search/multi?query={text}&platforms={a,b,c}
GET https://api.onekey.dev/v1/creator/card?handle={handle}&platforms={a,b,c}
x-api-key: YOUR_API_KEY

When to use it#

  • Search: "what is being said about X" across communities, or "which packages are called Y" across registries, in one call rather than one per platform.
  • Creator card: the same person or project across the platforms they use, given one handle.
  • Both when you want per-platform outcomes without one platform's failure hiding the others.

For arbitrary mixes of endpoints and parameters, use batch instead.

Parameters#

ParameterEndpointRequiredWhat it is
query/v1/search/multiyesThe search text, at most 200 characters, sent as each search endpoint's query parameter.
handle/v1/creator/cardyesThe username or handle, at most 200 characters. Each profile endpoint applies its own normalisation (a leading @ is removed where the endpoint does that).
platformsbothyesComma-separated platform ids from the tables below, up to 20. Case does not matter, duplicates are collapsed, and the order is the order of the legs.
fuse/v1/search/multinorrf adds a single merged list (see Rank fusion). Any other value is 422 INVALID_PARAMS; on /v1/creator/card it is refused.
includebothnoReserved for optional enrichment steps that are not available yet. Any value is refused with 422 INVALID_PARAMS rather than silently ignored.
curl "https://api.onekey.dev/v1/search/multi?query=rust&platforms=hackernews,bluesky,gitlab" \
  -H "x-api-key: YOUR_API_KEY"

data is an object keyed by platform id, one row per requested platform. Recorded from the live API, lists shortened:

json
{
  "success": true,
  "data": {
    "hackernews": {
      "index": 0, "platform": "hackernews", "endpoint": "search", "params": { "q": "rust" },
      "status": 200,
      "data": [ { "content": { "id": "22238335", "title": "Why Discord is switching from Go to Rust", "url": "https://blog.discordapp.com/...", "createdAt": "2020-02-04T17:30:40Z" }, "engagement": { "likes": 1582, "comments": 642 }, "author": { "username": "Sikul" }, "computed": { "engagement_rate": null, "language": "en" } }, "...9 more" ],
      "error": null, "credits_charged": 5, "cached": false,
      "pagination": { "next_cursor": "eyJiIjoiNGJlODQ0YmYwN2IzIiwicCI6IjEiLCJzIjoiaGFja2VybmV3cy5zZWFyY2giLCJ2IjoxfQ.UeYSZjaLLB5UZeSakGfkew", "has_more": true, "page_size": 10 }
    },
    "bluesky": {
      "index": 1, "platform": "bluesky", "endpoint": "search", "params": { "q": "rust" },
      "status": 200,
      "data": [ { "author": { "username": "rusty.todayintabs.com", "displayName": "Rusty Foster", "...": "..." }, "content": { "text": "..." } }, "...9 more" ],
      "error": null, "credits_charged": 5, "cached": false,
      "pagination": { "next_cursor": "...", "has_more": true, "page_size": 10 }
    },
    "gitlab": {
      "index": 2, "platform": "gitlab", "endpoint": "search", "params": { "q": "rust" },
      "status": 200,
      "data": [ { "content": { "id": "10174980", "title": "veloren", "url": "https://gitlab.com/veloren/veloren", "...": "..." }, "engagement": { "likes": 2399, "shares": 817 }, "author": { "username": "veloren" } }, "...9 more" ],
      "error": null, "credits_charged": 5, "cached": false
    }
  },
  "metadata": {
    "recipe": "search_multi", "query": "rust", "platforms": ["hackernews", "bluesky", "gitlab"],
    "legs": 3, "succeeded": 3, "failed": 0, "credits_charged": 15,
    "request_id": "81a469769d884da7b47b7fa118d50766", "idempotent_replay": false
  },
  "error": null
}

Each platform's list is that endpoint's first page, in the platform's own order. endpoint names the leg that ran, so you can read its page for the fields it returns. To page further, call that endpoint directly with its next_cursor (for the row above: GET /v1/hackernews/search?q=rust&cursor=...); a cursor is bound to the endpoint and its query, not to this call.

Platforms with a search leg#

19 platforms. Each platform has exactly one endpoint in this role, generated here from the same declaration the API uses; x-platforms on /v1/search/multi in /openapi.json and features.search in GET /v1/platforms are the live list. Read the third column: some legs search people, packages or channels rather than posts, and the same query means whatever that endpoint makes of it.

PlatformEndpointWhat the leg searchesPrice
blueskybluesky.searchSearch Bluesky users by keyword1 credit
cratescrates.searchSearch crates.io5 credits
devtodevto.searchList popular DEV articles for a tag5 credits
dockerhubdockerhub.searchSearch Docker Hub repositories5 credits
githubgithub.search_reposSearch GitHub repositories (most starred first)5 credits
gitlabgitlab.searchSearch public GitLab projects (most starred first)5 credits
hackernewshackernews.searchSearch Hacker News stories by keyword1 credit
huggingfacehuggingface.modelsSearch Hugging Face models5 credits
lemmylemmy.search_postsSearch Lemmy posts5 credits
mastodonmastodon.search_accountsSearch Mastodon accounts5 credits
npmnpm.searchSearch the npm registry (with popularity and quality scores)5 credits
redditreddit.search_postsSearch Reddit posts2 credits
soundcloudsoundcloud.search_tracksSearch SoundCloud tracks by keyword3 credits
stackexchangestackexchange.searchSearch Stack Exchange questions5 credits
tiktoktiktok.search_videosSearch TikTok videos by keyword2 credits
twitchtwitch.search_channelsSearch Twitch channels5 credits
wikipediawikipedia.searchSearch Wikipedia articles (English)5 credits
xx.searchSearch recent tweets by keyword or hashtag2 credits
youtubeyoutube.search_videosSearch YouTube videos by keyword2 credits

Creator card#

curl "https://api.onekey.dev/v1/creator/card?handle=torvalds&platforms=github,gitlab,bluesky" \
  -H "x-api-key: YOUR_API_KEY"

Recorded from the live API. One leg succeeded (from cache), one handle does not exist on GitLab, and the Bluesky source failed for this handle (a bare name is not a full Bluesky handle), which is the normal shape of a partial success:

json
{
  "success": true,
  "data": {
    "github": {
      "index": 0, "platform": "github", "endpoint": "profile", "params": { "username": "torvalds" },
      "status": 200,
      "data": { "author": { "id": "1024025", "username": "torvalds", "displayName": "Linus Torvalds", "followers": 325709, "following": 0, "url": "https://github.com/torvalds", "avatarUrl": "https://avatars.githubusercontent.com/u/1024025?v=4" }, "engagement": { "comments": 12 }, "content": { "text": null }, "computed": { "language": null } },
      "error": null, "credits_charged": 0, "cached": true
    },
    "gitlab": {
      "index": 1, "platform": "gitlab", "endpoint": "user", "params": { "username": "torvalds" },
      "status": 404, "data": null,
      "error": { "code": "not_found", "type": "NOT_FOUND", "message": "Not found", "doc_url": "https://onekey.dev/errors/NOT_FOUND" },
      "credits_charged": 0, "cached": false
    },
    "bluesky": {
      "index": 2, "platform": "bluesky", "endpoint": "profile", "params": { "handle": "torvalds" },
      "status": 502, "data": null,
      "error": { "code": "upstream_error", "type": "UPSTREAM_ERROR", "message": "The upstream platform failed to respond", "doc_url": "https://onekey.dev/errors/UPSTREAM_ERROR" },
      "credits_charged": 0, "cached": false
    }
  },
  "metadata": {
    "recipe": "creator_card", "handle": "torvalds", "platforms": ["github", "gitlab", "bluesky"],
    "legs": 3, "succeeded": 1, "failed": 2, "credits_charged": 0,
    "request_id": "22b51f3ee0854bedac053b0764811813", "idempotent_replay": false
  },
  "error": null
}

Platforms with a profile leg#

17 platforms. The same handle is sent to each endpoint's handle parameter, so a name that is spelled differently on one platform simply comes back 404 there.

PlatformEndpointWhat the leg fetchesPrice
blueskybluesky.profileFetch a Bluesky user's public profile1 credit
cratescrates.userPublic profile of a crates.io user1 credit
devtodevto.profileFetch a DEV (dev.to) user's public profile1 credit
githubgithub.profileFetch a GitHub user's public profile1 credit
gitlabgitlab.userGitLab.com public user profile1 credit
hackernewshackernews.userFetch a Hacker News user profile1 credit
huggingfacehuggingface.userHugging Face user profile1 credit
instagraminstagram.profileInstagram profile: bio, followers, following, post count, verification, avatar2 credits
lemmylemmy.userA Lemmy user's profile1 credit
lobsterslobsters.userFetch a Lobsters user profile1 credit
mastodonmastodon.profileFetch a Mastodon account (default instance mastodon.social)1 credit
redditreddit.user_aboutA Reddit user's profile1 credit
soundcloudsoundcloud.profileSoundCloud artist page card: name, bio snippet, avatar1 credit
tiktoktiktok.profileTikTok user profile: nickname, bio, followers, following, likes, video count, secUid2 credits
twitchtwitch.profileTwitch channel profile: display name, bio, followers, avatar, creation date1 credit
xx.userPublic profile for an X handle: followers, following, bio, avatar1 credit
youtubeyoutube.channel_by_handleYouTube channel details by @handle2 credits

The rows#

FieldTypeMeaning
indexnumberPosition of this item in your request, from 0.
platform, endpointstringThe endpoint that ran.
paramsobjectThe parameters it ran with, every value as a string.
statusnumberThe HTTP status this call would have had on its own: 200, or 404, 422, 502...
dataobject, array or nullThe unified result when status is 200, exactly as the endpoint returns it (including its computed block where it has one). null otherwise.
errorobject or nullnull on success. Otherwise code, type, message, doc_url, as in any error.
credits_chargednumberWhat this item cost. 0 for a cache hit or a failure.
cachedbooleantrue when the answer came from cache.
paginationobjectOnly when the endpoint pages: its next_cursor, has_more, page_size. To get the next page, call the endpoint directly with that cursor (see Pagination).

Metadata#

FieldMeaning
recipesearch_multi or creator_card.
query or handleYour input, trimmed.
platformsThe platforms as run, in leg order.
legs, succeeded, failedHow many legs ran, how many rows have status: 200, and how many do not.
credits_chargedThe sum of the rows' credits_charged.
fuseOnly with fuse=rrf: {"method": "rrf", "k": 60}.
request_id, idempotent_replayAs on every response.

Partial success#

  • A 200 means the call ran. Some rows may still be failures; metadata.succeeded and failed count them, and each failed row carries its own error.
  • When every leg fails, the call fails with the first leg's error (in platforms order): a 404 if the first platform found nothing, a 502 if its source failed, and so on. Nothing is charged, since every leg was refunded.
  • A platform with no endpoint in that role is refused before anything runs: 422 UNSUPPORTED_PLATFORM, naming the platform(s), free. Check the tables above or GET /v1/platforms rather than guessing.
  • A missing query, handle or platforms, more than 20 platforms, an input over 200 characters, or a reserved name such as fused given as a platform is 422 INVALID_PARAMS, free.
shell
curl "https://api.onekey.dev/v1/search/multi?query=rust&platforms=hackernews,myspace" \
  -H "x-api-key: YOUR_API_KEY"
# HTTP/1.1 422 Unprocessable Content
json
{
  "success": false,
  "data": null,
  "metadata": { "request_id": "b030900d263840c491b87f8c2a4450de" },
  "error": {
    "code": "unsupported_platform",
    "type": "UNSUPPORTED_PLATFORM",
    "message": "No search endpoint for platform(s): myspace",
    "request_id": "b030900d263840c491b87f8c2a4450de",
    "doc_url": "https://onekey.dev/errors/UNSUPPORTED_PLATFORM"
  }
}

What it costs#

The sum of the legs that succeeded, each at its endpoint's own price (the last column of the tables; endpoints cost 1 to 8 credits). A leg served from cache is free (a cached response over 100 KB costs 0.2 credit), and failed legs are refunded. Three search legs at 5 credits each is 15 credits, as in the recorded response above; the creator card above cost 0 because its only successful leg was a cache hit. Fusion costs nothing extra.

Before anything runs, Onekey adds up the most every item could cost (each endpoint's price, or for an endpoint with more than one data route the price of its dearest one) and refuses the whole call if that sum exceeds your balance (402 INSUFFICIENT_CREDITS) or the key's remaining credit limit (402 KEY_BUDGET_EXCEEDED). Nothing is run or charged, and the message states both numbers, for example This call could cost up to 15 credits; the account balance is 10 credits. Nothing was run or charged (for a key limit: ... this key has 4 of its 500-credit budget left ...). The check does not look at the cache, so items that would have been free cache hits still count at full price here. Keep enough balance for the worst case even when you expect most items to be cached.

Rate limit: one call is one request against your per-minute allowance, however many platforms it fans out to. An Idempotency-Key covers the whole call, bound to the query string (input, platforms and fuse); a replay returns the stored rows with credits_charged: 0. A call that fails as a whole releases the key.

Rank fusion: fuse=rrf#

Add fuse=rrf to /v1/search/multi and the answer gains data.fused: one merged list built by reciprocal rank fusion. It is plain arithmetic over the order each platform returned, no text analysis and no judgement of relevance on our side.

The rule: for every leg that succeeded, the item at 1-based position r in that platform's list scores

text
score = 1 / (k + r)        with k = 60

so rank 1 scores 0.016393, rank 2 scores 0.016129, rank 10 scores 0.014286. An item that appears on several platforms adds up its scores: first on one platform and fourth on another is 0.016393 + 0.015625 = 0.032018, which beats anything that is first on a single platform. The constant 60 keeps the difference between neighbouring ranks small, so appearing on two platforms matters more than being one place higher on one of them.

shell
curl "https://api.onekey.dev/v1/search/multi?query=rust&platforms=hackernews,devto&fuse=rrf" \
  -H "x-api-key: YOUR_API_KEY"
json
{
  "success": true,
  "data": {
    "hackernews": { "index": 0, "status": 200, "data": [ "...10 stories..." ], "credits_charged": 0, "cached": true, "...": "..." },
    "devto": { "index": 1, "status": 200, "data": [ "...10 articles..." ], "credits_charged": 5, "cached": false, "...": "..." },
    "fused": [
      { "score": 0.016393, "platform": "hackernews", "platforms": ["hackernews"], "ranks": { "hackernews": 1 }, "key": "url", "item": { "content": { "title": "Why Discord is switching from Go to Rust", "...": "..." }, "...": "..." } },
      { "score": 0.016393, "platform": "devto", "platforms": ["devto"], "ranks": { "devto": 1 }, "key": "url", "item": { "...": "..." } },
      { "score": 0.016129, "platform": "hackernews", "platforms": ["hackernews"], "ranks": { "hackernews": 2 }, "key": "url", "item": { "...": "..." } },
      "...17 more"
    ]
  },
  "metadata": { "recipe": "search_multi", "query": "rust", "platforms": ["hackernews", "devto"], "fuse": { "method": "rrf", "k": 60 }, "legs": 2, "succeeded": 2, "failed": 0, "credits_charged": 5, "request_id": "17de1a377b09486399b279799307b805", "idempotent_replay": false },
  "error": null
}
Fused entry fieldMeaning
scoreThe summed reciprocal-rank score, 6 decimals.
itemThe unified item, exactly as it appears in its platform's row (the copy from the first platform that had it).
platformThe first platform (in your platforms order) that returned it.
platformsEvery platform that returned it.
ranksIts 1-based position in each of those platforms' lists.
keyHow identity was decided: url, id or pos (below).

Which items count as the same thing. Identity is the canonical content.url when the endpoint maps one: scheme and host lower-cased, a leading www. dropped, no fragment, no trailing slash. That is the only key that merges across platforms, so a story on one platform and an article on another merge only when they link to the same page. Items without a URL are keyed by platform:content.id and never merge across platforms; items with neither are keyed by their position. The same URL twice in one platform's list counts once, at its first rank.

Order. By score descending; ties are broken by the best rank the item reached, then by the position of its first platform in your platforms order, then by that rank. The result is fully deterministic: the same rows always fuse the same way.

The per-platform rows are unchanged, metadata.fuse records the method and k, and the price is the same as without fuse. A failed leg simply contributes nothing. When streaming, the fused list arrives as one fused frame before done (see Streaming).

Honest limits#

  • First page only. Each leg is one page of its endpoint (its page_size). Page further on the endpoint itself.
  • A search leg is whatever that platform's search endpoint is. On some platforms it searches users, packages, models or channels rather than posts; the table says which. Results are not filtered or reranked by us.
  • No cross-platform relevance. Side by side, the rows are in each platform's own order. fuse=rrf merges by rank position only; a platform that returns weak results in a confident order still contributes its ranks.
  • Fusion rarely merges across platforms. Most items carry a platform-specific URL (key: "url" but a URL only that platform has) or no URL at all, so platforms usually has one entry. Merging happens when several platforms link to the same outside page.
  • A handle is not an identity. The creator card sends the same string to every platform; it does not verify that the accounts belong to the same person.

Errors it can return#

For the call as a whole (nothing is run or charged):

StatusTypeWhen
422UNSUPPORTED_PLATFORMA requested platform has no endpoint in that role.
422INVALID_PARAMSquery, handle or platforms missing; more than 20 platforms; input over 200 characters; a reserved platform name; a bad fuse value, fuse on /v1/creator/card, or any include value.
402INSUFFICIENT_CREDITS, KEY_BUDGET_EXCEEDEDThe worst-case total is above your balance or the key's limit.
401UNAUTHORIZEDBad or missing key.
429RATE_LIMITEDWait Retry-After seconds.
AnyThe first leg's errorEvery leg failed: the call takes the status and error of the first leg, most often NOT_FOUND or UPSTREAM_ERROR.

Inside a row, status and error.type can be anything a single call returns, most often NOT_FOUND and the UPSTREAM_* types. Plus the idempotency answers when you send an Idempotency-Key. See Errors for the full list.

Try it: Creator card