Bluesky Post Search API. Posts matching your search.
Posts by keyword, hashtag, author or link. We handle the proxies, blocks and platform changes.
Bluesky Post Search API at a glance
Returns public Bluesky posts that match your words, newest or top first, filtered by date, language, author, mention, hashtag or site. Each post has its link, publish time, full text with hashtags, mentions and full link URLs, images and video, link card, quote and reply, and exact like, reply, repost, quote and bookmark counts. Page back through the newest-first results with the cursor.
GET /v1/bluesky/search_posts
- 1 credit per page, $0.75–$1.97 per 1,000 pages
- 22 typed fields null when unknown
- 100 posts per page, newest first, by the time each post states, to the second
queryis all it needs, e.g.climate change
Send one request. Get up to 100 posts back.
Copy the request in your language, or hand it to your agent. Sign up for an API key and your first 100 credits are free.
$ npm install socialtapquery = climate changeconst params = new URLSearchParams({
query: "bitcoin",
sort: "newest",
});
const res = await fetch(`https://socialtap.dev/v1/bluesky/search_posts?${params}`, {
headers: { "x-api-key": process.env.SOCIALTAP_API_KEY ?? "" },
});
const body = await res.json();
if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`);
const { data, page, meta } = body;
console.log(data, meta.credits_charged);import os
import requests
res = requests.get(
"https://socialtap.dev/v1/bluesky/search_posts",
params={"query": "bitcoin", "sort": "newest"},
headers={"x-api-key": os.environ["SOCIALTAP_API_KEY"]},
)
body = res.json()
if not res.ok:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
data, page, meta = body["data"], body["page"], body["meta"]
print(data, meta["credits_charged"])require "json"
require "net/http"
uri = URI("https://socialtap.dev/v1/bluesky/search_posts")
uri.query = URI.encode_www_form('query' => 'bitcoin', 'sort' => 'newest')
req = Net::HTTP::Get.new(uri)
req["x-api-key"] = ENV.fetch("SOCIALTAP_API_KEY")
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") { |http| http.request(req) }
body = JSON.parse(res.body)
raise "#{body["error"]["code"]}: #{body["error"]["message"]}" unless res.is_a?(Net::HTTPSuccess)
pp body["data"]
puts body["meta"]["credits_charged"]<?php
$query = http_build_query([
'query' => 'bitcoin',
'sort' => 'newest',
]);
$ch = curl_init('https://socialtap.dev/v1/bluesky/search_posts?' . $query);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['x-api-key: ' . getenv('SOCIALTAP_API_KEY')],
]);
$body = json_decode(curl_exec($ch), true);
if (curl_getinfo($ch, CURLINFO_RESPONSE_CODE) >= 400) {
throw new RuntimeException($body['error']['code'] . ': ' . $body['error']['message']);
}
print_r($body['data']);
echo $body['meta']['credits_charged'], PHP_EOL;package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"net/url"
"os"
)
func main() {
q := url.Values{}
q.Set("query", "bitcoin")
q.Set("sort", "newest")
req, err := http.NewRequest("GET", "https://socialtap.dev/v1/bluesky/search_posts?"+q.Encode(), nil)
if err != nil {
log.Fatal(err)
}
req.Header.Set("x-api-key", os.Getenv("SOCIALTAP_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
var body struct {
Data json.RawMessage `json:"data"`
Page json.RawMessage `json:"page"`
Meta map[string]any `json:"meta"`
Error *struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
if err := json.NewDecoder(res.Body).Decode(&body); err != nil {
log.Fatal(err)
}
if body.Error != nil {
log.Fatalf("%s: %s", body.Error.Code, body.Error.Message)
}
fmt.Println(string(body.Data), body.Meta["credits_charged"])
}curl 'https://socialtap.dev/v1/bluesky/search_posts?query=bitcoin&sort=newest' \
-H "x-api-key: $SOCIALTAP_API_KEY"{
"data": [
{
"approximate_fields": [],
"author": {
"avatar_url": "https://cdn.bsky.app/img/avatar/plain/did:plc:hlod3gl5ewwglnr4yarddcrk/bafkreihy5kd5vnqqs77wtxzdf5kdyq24x23vtawwb5knkz7wh5yesamsoy",
"display_name": "Swiss Whale Intelligence 🇨🇭",
"id": "did:plc:hlod3gl5ewwglnr4yarddcrk",
"is_verified": false,
"url": "https://bsky.app/profile/btcwhalealerts.com",
"username": "btcwhalealerts.com"
},
"coauthors": [],
"duration_ms": null,
"hashtags": [
"Bitcoin",
"Crypto"
],
"id": "at://did:plc:hlod3gl5ewwglnr4yarddcrk/app.bsky.feed.post/3mzjyfostuj2n",
"is_nsfw": false,
"is_pinned": null,
"is_sponsored": null,
"language": "en",
"media": [],
"mentions": [],
"metrics": {
"comment_count": 0,
"like_count": 0,
"quote_count": 0,
"repost_count": 0,
"save_count": 0,
"share_count": null,
"view_count": null
},
"platform": "bluesky",
"platform_specific": {
"bluesky": {
"bridged_from_url": null,
"cid": "bafyreiduygaoktbbpfbii3ds5d4nkmu5c3yfjwqvlwkeqzrhoxa5gxpgvy",
"created_at": "2026-11-05T04:04:24Z",
"embedded_record_uri": null,
"indexed_at": "2026-11-05T04:04:24Z",
"labels": [],
"languages": [
"en"
],
"link_card": null,
"links": [
"https://swisswhaleintelligence.com/tx/8b044d3165b582ac112c14b3afc7ff017dba8bd3f124b03dbbf9912a27e0bfaa"
],
"quoted_post": null,
"quoted_post_id": null,
"reply_root_post_id": null,
"reply_to_post_id": null,
"reposted_at": null,
"reposted_by": null
}
},
"platform_type": null,
"published_at": "2026-11-05T04:04:24Z",
"text": "🐳 4,259 BTC ($343.7M) whale transfer\nPast similar activity: avg -2.2% price drop 72h after.\n\nhttps://swisswhaleintelligence.com/tx/8b044d3165b582ac112c14b3afc7ff017dba8bd3f124b03dbbf9912a27e0bfaa #Bitcoin #Crypto",
"thumbnail_url": null,
"title": null,
"type": "text",
"unknown_fields": [],
"url": "https://bsky.app/profile/btcwhalealerts.com/post/3mzjyfostuj2n"
},
{
"approximate_fields": [],
"author": {
"avatar_url": "https://cdn.bsky.app/img/avatar/plain/did:plc:pnmlwvud7rkanzzns5nanon6/bafkreibjn6xzku5qrjmp4s7abkf753wh5h7youodu7jgynnwtnwtbl2ciu",
"display_name": "Pandavana Crypto News",
"id": "did:plc:pnmlwvud7rkanzzns5nanon6",
"is_verified": false,
"url": "https://bsky.app/profile/pandacrypto.bsky.social",
"username": "pandacrypto.bsky.social"
},
"coauthors": [],
"duration_ms": null,
"hashtags": [
"Crypto",
"Solana"
],
"id": "at://did:plc:pnmlwvud7rkanzzns5nanon6/app.bsky.feed.post/3mzjyf2labq2g",
"is_nsfw": false,
"is_pinned": null,
"is_sponsored": null,
"language": null,
"media": [],
"mentions": [],
"metrics": {
"comment_count": 0,
"like_count": 0,
"quote_count": 0,
"repost_count": 0,
"save_count": 0,
"share_count": null,
"view_count": null
},
"platform": "bluesky",
"platform_specific": {
"bluesky": {
"bridged_from_url": null,
"cid": "bafyreibohrvfnua3lpwuaqnnfssupil4q64mrmtzlktlqchfuqzrnubx3e",
"created_at": "2026-11-05T04:04:02Z",
"embedded_record_uri": null,
"indexed_at": "2026-11-05T04:04:03Z",
"labels": [],
"languages": [],
"link_card": null,
"links": [],
"quoted_post": null,
"quoted_post_id": null,
"reply_root_post_id": null,
"reply_to_post_id": null,
"reposted_at": null,
"reposted_by": null
}
},
"platform_type": null,
"published_at": "2026-11-05T04:04:02Z",
"text": "#Crypto Prices:\r\n\r\nBitcoin $80,414.50\r\nEthereum $2,418.80\r\nXRP $1.31\r\n#Solana $96.16\r\nSui $0.93\r\n\r\n\r\n",
"thumbnail_url": null,
"title": null,
"type": "text",
"unknown_fields": [],
"url": "https://bsky.app/profile/pandacrypto.bsky.social/post/3mzjyf2labq2g"
},
{
"approximate_fields": [],
"author": {
"avatar_url": "https://cdn.bsky.app/img/avatar/plain/did:plc:vjpyc66fyniyqp2v6j7mxdvm/bafkreicg6htpnqv6cih6dhdxemkzugqmg4sqpgsfys5stz74ckwocynnoi",
"display_name": "miatone.bsky.social",
"id": "did:plc:vjpyc66fyniyqp2v6j7mxdvm",
"is_verified": false,
"url": "https://bsky.app/profile/miatone.bsky.social",
"username": "miatone.bsky.social"
},
"coauthors": [],
"duration_ms": null,
"hashtags": [],
"id": "at://did:plc:vjpyc66fyniyqp2v6j7mxdvm/app.bsky.feed.post/3mzjya6ieog2l",
"is_nsfw": false,
"is_pinned": null,
"is_sponsored": null,
"language": null,
"media": [],
"mentions": [],
"metrics": {
"comment_count": 0,
"like_count": 0,
"quote_count": 0,
"repost_count": 0,
"save_count": 0,
"share_count": null,
"view_count": null
},
"platform": "bluesky",
"platform_specific": {
"bluesky": {
"bridged_from_url": null,
"cid": "bafyreib4vxlqmmhsycxfkhzj27k4466dgooh2rbmiod3gqf2bey3ryiqxu",
"created_at": "2026-11-05T04:01:18Z",
"embedded_record_uri": null,
"indexed_at": "2026-11-05T04:01:19Z",
"labels": [],
"languages": [],
"link_card": null,
"links": [],
"quoted_post": null,
"quoted_post_id": null,
"reply_root_post_id": null,
"reply_to_post_id": null,
"reposted_at": null,
"reposted_by": null
}
},
"platform_type": null,
"published_at": "2026-11-05T04:01:18Z",
"text": "This caught my attention today: Nearly $1 Billion in Crypto Liquidations Hit as Bitcoin Jumps 4.6%",
"thumbnail_url": null,
"title": null,
"type": "text",
"unknown_fields": [],
"url": "https://bsky.app/profile/miatone.bsky.social/post/3mzjya6ieog2l"
}
],
"page": {
"has_more": true,
"next_cursor": "c1.EXAMPLE_CURSOR",
"next_cursor_expires_at": "2026-10-26T12:00:00Z",
"end_reason": null
},
"meta": {
"request_id": "req_3f9c2a7d1e8b4c6a9f0e2d4b6a8c0e1f",
"platform": "bluesky",
"source": "api",
"is_cached": false,
"credits_charged": 1,
"latency_ms": 560,
"fetched_at": "2026-10-25T12:00:00Z",
"input": null,
"credits_remaining": 9999,
"client_request_id": null,
"warnings": []
}
}import { SocialTap } from "socialtap";
const client = new SocialTap(); // apiKey defaults to process.env.SOCIALTAP_API_KEY
const res = await client.bluesky.searchPosts.list({ query: "bitcoin", sort: "newest" });
console.log(res.data);
console.log(res.credits.charged, res.meta.request_id);import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
public class Main {
public static void main(String[] args) throws Exception {
String query = "query=" + encode("bitcoin")
+ "&sort=" + encode("newest");
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://socialtap.dev/v1/bluesky/search_posts?" + query))
.header("x-api-key", System.getenv("SOCIALTAP_API_KEY"))
.GET()
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
// An error is {"error": {"code", "message", ...}}; success is {data, meta}, meta.credits_charged its price.
if (response.statusCode() >= 400) throw new RuntimeException(response.body());
System.out.println(response.body());
}
static String encode(String value) {
return URLEncoder.encode(value, StandardCharsets.UTF_8);
}
}using System.Text.Json;
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("x-api-key", Environment.GetEnvironmentVariable("SOCIALTAP_API_KEY"));
var query = string.Join("&",
"query=" + Uri.EscapeDataString("bitcoin"),
"sort=" + Uri.EscapeDataString("newest"));
var res = await http.GetAsync("https://socialtap.dev/v1/bluesky/search_posts?" + query);
using var body = JsonDocument.Parse(await res.Content.ReadAsStringAsync());
if (!res.IsSuccessStatusCode)
{
var error = body.RootElement.GetProperty("error");
throw new Exception($"{error.GetProperty("code")}: {error.GetProperty("message")}");
}
Console.WriteLine(body.RootElement.GetProperty("data"));
Console.WriteLine(body.RootElement.GetProperty("meta").GetProperty("credits_charged"));// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }, serde_json = "1"
use std::env;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let res = reqwest::blocking::Client::new()
.get("https://socialtap.dev/v1/bluesky/search_posts")
.query(&[("query", "bitcoin"), ("sort", "newest")])
.header("x-api-key", env::var("SOCIALTAP_API_KEY")?)
.send()?;
let ok = res.status().is_success();
let body: serde_json::Value = res.json()?;
if !ok {
return Err(format!("{}: {}", body["error"]["code"], body["error"]["message"]).into());
}
println!("{} {}", body["data"], body["meta"]["credits_charged"]);
Ok(())
}200 OK · GET https://socialtap.dev/v1/bluesky/search_postsLong arrays and strings are shortened here. Real responses are complete.Docs One schema on 21 platforms.
The response reference lists every field, its type and when it is null. See the full schema.
Null means unknown
A field Bluesky doesn't show is null, never zero or an empty string, so a real zero always means zero.
What it accepts
Words to find posts by, as Bluesky's search reads them: a topic, a phrase in quotes, a #hashtag or an @handle, and a leading - leaves a word out (climate change, "coffee roaster" -bean, #atproto).
Write one parser
Bluesky, TikTok, Instagram and 18 more platforms answer in these fields, so the code that reads one reads them all.
platformstring- Platform ID (tiktok, youtube, …).
idstring- Platform-native post ID (string; Instagram shortcode, Reddit with its t3_ prefix).
urlstring- Canonical post URL.
typestring- Normalized content shape. New values may be added; treat unknown values as other.
platform_typestring- The platform's own product label, lowercase (reel, short, story, live, tweet, …).
titlestring- Headline (YouTube title, Reddit title); null on caption-only platforms.
textstring- Caption, description or body as plain text.
languagestring- BCP-47 language of text, when the platform states it.
Live data from 33 platforms.
What teams build with the Bluesky Post Search API.
Track what people say about a brand
Collect posts with a hashtag or mention
Search one account's posts for a subject
Find who links to a site or page
Study a subject by language and date
Good to know before you build.
- Results come from Bluesky's own search, read without a login. Bluesky's app offers search only to signed-in people, so Bluesky could close this route, and the endpoint would then answer from the web search fallback below.
- When Bluesky's search doesn't answer, the first page comes from a public web search's index instead, with each post it finds read from Bluesky, so its fields stay full. A partial_data warning names the sort and filters it couldn't fully apply. That list holds only what the index has, often a small part of what Bluesky has and very little on rare words, non-Latin text or recent dates, in the engine's ranking rather than the order you asked for.
- If Bluesky's other routes are down too, the posts are what the index shows of them: id names the author as the link does, type reads text, text is the index's copy and can be cut short, the time comes from the post's record key, and the warning lists the fields left null. Repeat the request later for the full answer.
- A next_cursor continues the list it came from. If Bluesky's search stops answering partway through a walk, that page fails with a retryable error rather than continue on a different list. Start without the cursor for the fallback's list.
- A post whose author chose to be seen only by signed-in people is left out, as everywhere on Bluesky without a login, so a page can hold fewer posts than its limit, or none, and still have a next_cursor. On some searches that is a large share of the results, above all in Japanese. Bluesky's search also hides a few posts of its own.
- sort=newest goes by the time each post states, to the second, as Bluesky's search does, so a post imported from elsewhere or with a wrong clock can sit out of order by published_at. since and until filter by that time and then by published_at, so an imported post whose published_at lies outside them is left out.
- When more posts share one second than a page holds, the walk goes on past that second without the rest, and that page says so with an items_omitted warning.
- sort=popular serves one page, because Bluesky doesn't serve its Top ranking further, and the page ends with platform_limit.
- An author, mention or tag nobody has returns an empty page, not an error, as Bluesky's search does. mentions matches posts that really link the account, and an @handle in the query is read the same way.
- Case does not matter. The query's own site: operators are removed, and intitle:, inurl:, in: and operators like them are read as the plain words they name.
1 credit per page. Failed requests are free.
Buy credits once and spend them on any endpoint, on any platform.
| Source | Price per successful page | Per 1,000 pages |
|---|---|---|
| Any source | 1 credit | $0.75–$1.97 |
- Cache hits are free.
- Credits from packs and top-ups never expire.
- A monthly plan's credits are spent first, and what is left of them ends at each renewal.
- Prices exclude tax: sales tax is added at checkout where it applies.
- Outside the US, plans and packs are sold to businesses: checkout asks for your VAT, GST or other business tax ID.
Questions about the Bluesky Post Search API.
More in the API reference.
Can I search by hashtag, author or link?
How much does the Bluesky Post Search API cost?
How do I get the next page?
Try it with your own search.
Type it in the playground, or copy the request into your code.
- No credit card
- Failed requests are free
- Works on all 218 endpoints