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.
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_KEYWhen 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#
| Parameter | Endpoint | Required | What it is |
|---|---|---|---|
query | /v1/search/multi | yes | The search text, at most 200 characters, sent as each search endpoint's query parameter. |
handle | /v1/creator/card | yes | The username or handle, at most 200 characters. Each profile endpoint applies its own normalisation (a leading @ is removed where the endpoint does that). |
platforms | both | yes | Comma-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/multi | no | rrf adds a single merged list (see Rank fusion). Any other value is 422 INVALID_PARAMS; on /v1/creator/card it is refused. |
include | both | no | Reserved for optional enrichment steps that are not available yet. Any value is refused with 422 INVALID_PARAMS rather than silently ignored. |
Multi-platform search#
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:
{
"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.
| Platform | Endpoint | What the leg searches | Price |
|---|---|---|---|
bluesky | bluesky.search | Search Bluesky users by keyword | 1 credit |
crates | crates.search | Search crates.io | 5 credits |
devto | devto.search | List popular DEV articles for a tag | 5 credits |
dockerhub | dockerhub.search | Search Docker Hub repositories | 5 credits |
github | github.search_repos | Search GitHub repositories (most starred first) | 5 credits |
gitlab | gitlab.search | Search public GitLab projects (most starred first) | 5 credits |
hackernews | hackernews.search | Search Hacker News stories by keyword | 1 credit |
huggingface | huggingface.models | Search Hugging Face models | 5 credits |
lemmy | lemmy.search_posts | Search Lemmy posts | 5 credits |
mastodon | mastodon.search_accounts | Search Mastodon accounts | 5 credits |
npm | npm.search | Search the npm registry (with popularity and quality scores) | 5 credits |
reddit | reddit.search_posts | Search Reddit posts | 2 credits |
soundcloud | soundcloud.search_tracks | Search SoundCloud tracks by keyword | 3 credits |
stackexchange | stackexchange.search | Search Stack Exchange questions | 5 credits |
tiktok | tiktok.search_videos | Search TikTok videos by keyword | 2 credits |
twitch | twitch.search_channels | Search Twitch channels | 5 credits |
wikipedia | wikipedia.search | Search Wikipedia articles (English) | 5 credits |
x | x.search | Search recent tweets by keyword or hashtag | 2 credits |
youtube | youtube.search_videos | Search YouTube videos by keyword | 2 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:
{
"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.
| Platform | Endpoint | What the leg fetches | Price |
|---|---|---|---|
bluesky | bluesky.profile | Fetch a Bluesky user's public profile | 1 credit |
crates | crates.user | Public profile of a crates.io user | 1 credit |
devto | devto.profile | Fetch a DEV (dev.to) user's public profile | 1 credit |
github | github.profile | Fetch a GitHub user's public profile | 1 credit |
gitlab | gitlab.user | GitLab.com public user profile | 1 credit |
hackernews | hackernews.user | Fetch a Hacker News user profile | 1 credit |
huggingface | huggingface.user | Hugging Face user profile | 1 credit |
instagram | instagram.profile | Instagram profile: bio, followers, following, post count, verification, avatar | 2 credits |
lemmy | lemmy.user | A Lemmy user's profile | 1 credit |
lobsters | lobsters.user | Fetch a Lobsters user profile | 1 credit |
mastodon | mastodon.profile | Fetch a Mastodon account (default instance mastodon.social) | 1 credit |
reddit | reddit.user_about | A Reddit user's profile | 1 credit |
soundcloud | soundcloud.profile | SoundCloud artist page card: name, bio snippet, avatar | 1 credit |
tiktok | tiktok.profile | TikTok user profile: nickname, bio, followers, following, likes, video count, secUid | 2 credits |
twitch | twitch.profile | Twitch channel profile: display name, bio, followers, avatar, creation date | 1 credit |
x | x.user | Public profile for an X handle: followers, following, bio, avatar | 1 credit |
youtube | youtube.channel_by_handle | YouTube channel details by @handle | 2 credits |
The rows#
| Field | Type | Meaning |
|---|---|---|
index | number | Position of this item in your request, from 0. |
platform, endpoint | string | The endpoint that ran. |
params | object | The parameters it ran with, every value as a string. |
status | number | The HTTP status this call would have had on its own: 200, or 404, 422, 502... |
data | object, array or null | The unified result when status is 200, exactly as the endpoint returns it (including its computed block where it has one). null otherwise. |
error | object or null | null on success. Otherwise code, type, message, doc_url, as in any error. |
credits_charged | number | What this item cost. 0 for a cache hit or a failure. |
cached | boolean | true when the answer came from cache. |
pagination | object | Only 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#
| Field | Meaning |
|---|---|
recipe | search_multi or creator_card. |
query or handle | Your input, trimmed. |
platforms | The platforms as run, in leg order. |
legs, succeeded, failed | How many legs ran, how many rows have status: 200, and how many do not. |
credits_charged | The sum of the rows' credits_charged. |
fuse | Only with fuse=rrf: {"method": "rrf", "k": 60}. |
request_id, idempotent_replay | As on every response. |
Partial success#
- A
200means the call ran. Some rows may still be failures;metadata.succeededandfailedcount them, and each failed row carries its ownerror. - When every leg fails, the call fails with the first leg's error (in
platformsorder): a404if the first platform found nothing, a502if 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 orGET /v1/platformsrather than guessing. - A missing
query,handleorplatforms, more than 20 platforms, an input over 200 characters, or a reserved name such asfusedgiven as a platform is422 INVALID_PARAMS, free.
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{
"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
score = 1 / (k + r) with k = 60so 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.
curl "https://api.onekey.dev/v1/search/multi?query=rust&platforms=hackernews,devto&fuse=rrf" \
-H "x-api-key: YOUR_API_KEY"{
"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 field | Meaning |
|---|---|
score | The summed reciprocal-rank score, 6 decimals. |
item | The unified item, exactly as it appears in its platform's row (the copy from the first platform that had it). |
platform | The first platform (in your platforms order) that returned it. |
platforms | Every platform that returned it. |
ranks | Its 1-based position in each of those platforms' lists. |
key | How 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=rrfmerges 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, soplatformsusually 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):
| Status | Type | When |
|---|---|---|
| 422 | UNSUPPORTED_PLATFORM | A requested platform has no endpoint in that role. |
| 422 | INVALID_PARAMS | query, 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. |
| 402 | INSUFFICIENT_CREDITS, KEY_BUDGET_EXCEEDED | The worst-case total is above your balance or the key's limit. |
| 401 | UNAUTHORIZED | Bad or missing key. |
| 429 | RATE_LIMITED | Wait Retry-After seconds. |
| Any | The first leg's error | Every 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