HonestHook Bluesky API

The Bluesky API from HonestHook — 2 operation(s) for bluesky.

Operations 4

GET /api/v1/bluesky/profile Public profile on bluesky, with the exact follower count #
GET /api/v1/bluesky/posts Recent posts on bluesky #
GET /api/v1/bluesky/post A single Bluesky post by its AT Protocol URI, with likes, reposts, replies and… #
GET /api/v1/bluesky/search Public Bluesky posts matching a term, ranked by top, with likes, reposts… #

Work with this as data

Every API here is available over the APIs.io API and to AI agents over MCP.

MCP server

One button, every client — Claude, Cursor, VS Code and the rest.

https://apis.io/mcp

Tools for apis

7 MCP tools reach this
  • find_apisBrowse and filter every API in the catalog.
  • get_api_artifactsOne API's artifacts, grouped by type.
  • get_openapiThe primary OpenAPI for this API.
  • find_similar_apisAPIs that look like this one.
  • apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
  • resolveTurn a domain, URL or GitHub org into the provider it belongs to.
  • find_cohortsEvery scored population of providers in the catalog.
All 92 tools →

Call it yourself

curl for this page
This API
curl "https://apis.io/api/v1/apis/honesthook:honesthook-bluesky-api"
All apis
curl "https://apis.io/api/v1/apis?limit=25"

Calls without X-API-Key need no key. Calls with it need a paid plan — get a key.

Get an API key

Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.

A second provider on the same verified email joins the account you already have.

OpenAPI Specification

honesthook-bluesky-api-openapi.yml Raw ↑
openapi: 3.2.0
info:
  title: HonestHook Bluesky API
  version: 1.0.0
  description: Profiles and posts from the major social networks, through one API. One key, one response shape; rate limits, retries and platform changes are on us. Clean JSON for your app or your AI agent. We read the exact field each platform publishes, and return null when none exists. Plus a historical trend archive that answers what was trending, which cannot be reconstructed after the fact.
  contact:
    url: https://honesthook.com
  license:
    name: Proprietary
    url: https://honesthook.com/docs
servers:
- url: https://honesthook.com
security:
- bearerAuth: []
tags:
- name: Bluesky
paths:
  /api/v1/bluesky/profile:
    get:
      operationId: blueskyProfile
      summary: Public profile on bluesky, with the exact follower count
      x-credits: 1
      x-charges-without-result: false
      description: 'One public profile, normalised into the same envelope as every other platform. Source: public.api.bsky.app (official, no key). Costs 1 credit; the answer is cached for an hour and a cache hit costs nothing (`cached: true`, `credits_used: 0`).


        AT Protocol handle, which is a domain name: `bsky.app`, `alguem.bsky.social`. Dots are part of it; slashes are refused.'
      parameters:
      - name: handle
        in: query
        required: true
        description: 'AT Protocol handle, which is a domain name: `bsky.app`, `alguem.bsky.social`. Dots are part of it; slashes are refused.'
        schema:
          type: string
          pattern: ^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)+$
          example: bsky.app
      responses:
        '200':
          description: Profile found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProfileEnvelope'
        '400':
          description: '`parametro_invalido` — the handle is missing or does not match this platform''s format. Nothing is charged.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProfileEnvelope'
        '401':
          description: '`chave_invalida` — key missing, unknown or inactive.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProfileEnvelope'
        '402':
          description: 'Out of credits. Two different types, and they mean different things: `sem_creditos` (both wallets are empty) and `teto_estourado` (the key has a monthly ceiling and hit it). Waiting does not help either one before the 1st — this is not a rate limit.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProfileEnvelope'
        '404':
          description: '`nao_encontrado` — no such profile on that platform.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProfileEnvelope'
        '451':
          description: '`bloqueado` — the platform refused us. The resource exists and YOUR key is fine; 403 would wrongly blame the caller.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProfileEnvelope'
        '503':
          description: '`indisponivel` — this endpoint is switched off in the catalogue, or a dependency of ours is down. Your key is fine.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProfileEnvelope'
      tags:
      - Bluesky
  /api/v1/bluesky/posts:
    get:
      operationId: blueskyPosts
      summary: Recent posts on bluesky
      x-credits: 1
      x-charges-without-result: false
      description: 'Posts recentes do proprio perfil no Bluesky, via app.bsky.feed.getAuthorFeed -- API publica oficial do AT Protocol, sem chave e sem conta. Sai pelo servico de fetch como as demais rotas, entao consome banda de proxy: ~93 KB com limit=25. Aceita ?limit= de 1 a 100, padrao 25; valor fora da faixa e recusado com 400 em vez de aparado, e cada limite tem a sua propria linha de cache. Repost de terceiro e excluido por autoria (o feed do bsky.app traz 1 em 20). Devolve likes, replies, reposts e quotes; nao devolve bookmarkCount (so o Bluesky tem) nem thumbnail (o embed de imagem ainda nao foi medido em amostra suficiente).'
      parameters:
      - name: handle
        in: query
        required: true
        description: 'AT Protocol handle, which is a domain name: `bsky.app`, `alguem.bsky.social`. Dots are part of it; slashes are refused.'
        schema:
          type: string
          pattern: ^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)+$
          example: bsky.app
      - name: limit
        in: query
        required: false
        description: 'How many posts to return, 1 to 100. A value outside the range is refused with 400, not clamped: the source clamps in silence, and a caller asking for more than the maximum would conclude the account has fewer posts than it does.'
        schema:
          type: integer
          minimum: 1
          maximum: 100
          default: 25
      responses:
        '200':
          description: 'Posts found. `has_more` differs by platform: on Bluesky it is computed from the cursor the source returns, so it can be true. On Mastodon it is false today because we do not yet read the `Link` header. On Threads it is null — the posts arrive inside the profile document and there is nothing to page, so we cannot know. `null` means unknown, never `no more`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PostsEnvelope'
        '400':
          description: '`parametro_invalido` — the handle is missing, does not match this platform''s format, or `limit` is outside the range. Nothing is charged.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PostsEnvelope'
        '401':
          description: '`chave_invalida` — key missing, unknown or inactive.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PostsEnvelope'
        '404':
          description: '`nao_encontrado` — no such account. Nothing is charged.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PostsEnvelope'
      tags:
      - Bluesky
  /api/v1/bluesky/post:
    get:
      operationId: blueskyPost
      summary: A single Bluesky post by its AT Protocol URI, with likes, reposts, replies and…
      description: 'A single Bluesky post, addressed by its AT Protocol URI (at://did:plc:.../app.bsky.feed.post/...). Returns the post as a one-item list, with likes, replies, reposts and quotes, plus the post author in the author envelope. Read from app.bsky.feed.getPostThread, the same public surface bluesky/profile uses -- no login, no third party. Measured on 2026-10-01 against the committed 289 KB thread fixture. Does NOT return the replies: the document carries 193 of them and we deliberately leave them out, because mixing a post with its replies in one list would make both unreadable; a replies endpoint would be its own call. There is no legitimate empty result here -- either the post was read or it was not, so a failure is an error and never an empty list, and a failed read is not charged. Followers and bio are null: this call does not carry them, which is different from the account not having them. title is null because a Bluesky post has none. Costs 1 credit and caches for an hour.'
      x-creditos: 1
      parameters:
      - name: uri
        in: query
        required: true
        schema:
          type: string
      responses:
        '200':
          description: Data found.
        '400':
          description: See the shared error shape.
        '401':
          description: See the shared error shape.
        '402':
          description: See the shared error shape.
        '404':
          description: See the shared error shape.
        '405':
          description: See the shared error shape.
        '451':
          description: See the shared error shape.
        '502':
          description: See the shared error shape.
        '503':
          description: See the shared error shape.
      tags:
      - Bluesky
  /api/v1/bluesky/search:
    get:
      operationId: blueskySearch
      summary: Public Bluesky posts matching a term, ranked by top, with likes, reposts…
      description: 'Public Bluesky posts matching a TERM, via app.bsky.feed.searchPosts -- the official AT Protocol API, no key and no account. FOURTH and last Bluesky path of the parity grid. THE HOST IS api.bsky.app, NOT public.api.bsky.app, and that difference is the whole endpoint: public.api.bsky.app answers 403 with an HTML block page on this route while answering 200 on getAuthorFeed, so the block is per-host, not the endpoint asking for an account -- our own archiver has searched through api.bsky.app since 06/09/2026. Results come from MANY authors, so unlike bluesky/posts there is no authorship filter; filtering by author here would always return empty. sort=top, not latest: on a popular term latest is almost all noise. hitsTotal IS NOT RETURNED -- measured, it reads 10000 for a common term and 0 for a nonexistent one, so it is a search cap dressed as a total, and serving it would promise a count nobody measured; sample_size says how many THIS call brought. Accepts ?limit= from 1 to 100, default 25; out of range is 400, never silently clamped -- measured, the source REFUSES limit=101 with 400 instead of trimming. has_more is Boolean(cursor) and nothing else. A term with no result is 200 with an empty items list -- that is an answer, and it is charged.'
      x-creditos: 1
      parameters:
      - name: q
        in: query
        required: true
        schema:
          type: string
      - name: limit
        in: query
        required: false
        schema:
          type: integer
      responses:
        '200':
          description: Data found.
        '400':
          description: See the shared error shape.
        '401':
          description: See the shared error shape.
        '402':
          description: See the shared error shape.
        '404':
          description: See the shared error shape.
        '405':
          description: See the shared error shape.
        '451':
          description: See the shared error shape.
        '502':
          description: See the shared error shape.
        '503':
          description: See the shared error shape.
      tags:
      - Bluesky
components:
  schemas:
    ProfileEnvelope:
      type: object
      required:
      - success
      - platform
      - endpoint
      - data
      - error
      - credits_used
      - credits_remaining
      - cached
      - request_id
      description: Identical shape on success and on error, so a client writes one parser. `success` is the only field that decides; `data` and `error` are mutually exclusive and the other is null, never absent.
      properties:
        success:
          type: boolean
        platform:
          type: string
        endpoint:
          type: string
        data:
          type:
          - object
          - 'null'
          properties:
            author:
              $ref: '#/components/schemas/Author'
            items:
              type: array
              items:
                type: object
            has_more:
              type:
              - boolean
              - 'null'
            next_cursor:
              type:
              - string
              - 'null'
        error:
          type:
          - object
          - 'null'
          properties:
            type:
              type: string
              enum:
              - parametro_invalido
              - chave_invalida
              - sem_creditos
              - teto_estourado
              - nao_encontrado
              - bloqueado
              - indisponivel
              description: Branch on this, never on the message text.
            message:
              type: string
            details:
              type:
              - object
              - 'null'
        credits_used:
          type: integer
        credits_remaining:
          type:
          - integer
          - 'null'
          description: 'THE SUM: purchased balance plus what is left of this month''s free allowance. This is the only number that answers "can I call again" — it is not 0 while you can still call. null means we could not read it, which is not the same as zero.'
        credits_purchased_remaining:
          type:
          - integer
          - 'null'
          description: Bought credits. These never expire.
        free_credits_remaining:
          type:
          - integer
          - 'null'
          description: What is left of this month's free allowance.
        free_credits_total:
          type:
          - integer
          - 'null'
          description: The monthly free allowance for this key.
        free_credits_reset_at:
          type: string
          format: date-time
          description: 'When the free allowance resets: 00:00 UTC on the 1st. It is here so the sum dropping at the turn of the month is expected rather than a surprise.'
        cached:
          type: boolean
          description: '`true` is a promise that we did NOT charge: it always comes with `credits_used: 0`.'
        request_id:
          type: string
    PostItem:
      type: object
      properties:
        id:
          type: string
        url:
          type:
          - string
          - 'null'
        text:
          type:
          - string
          - 'null'
        text_html:
          type:
          - string
          - 'null'
          description: Mastodon only. Third-party HTML, unsanitised — sanitise before rendering or you have an XSS hole. `text` is the same content already stripped.
        title:
          type: 'null'
          description: 'Always null: a status has no title.'
        position:
          type: 'null'
          description: 'Always null: chronological order is not a ranking.'
        published_at:
          type:
          - string
          - 'null'
          format: date-time
        thumbnail_url:
          type:
          - string
          - 'null'
          description: Null on bluesky and mastodon — the image embed has not been measured on a large enough sample to promise a shape.
        metrics:
          type: object
          properties:
            likes:
              type:
              - integer
              - 'null'
            replies:
              type:
              - integer
              - 'null'
            reposts:
              type:
              - integer
              - 'null'
            quotes:
              type:
              - integer
              - 'null'
              description: 'Null on threads: the source publishes none.'
    Author:
      type: object
      description: 'The same twelve names on every platform. A field that does not exist there is null, never absent and never zero: zero is a fact about the account, null is a fact about our reading.'
      properties:
        id:
          type:
          - string
          - 'null'
          description: Stable identifier on that platform.
        handle:
          type:
          - string
          - 'null'
        name:
          type:
          - string
          - 'null'
        bio:
          type:
          - string
          - 'null'
        avatar_url:
          type:
          - string
          - 'null'
        followers:
          type:
          - integer
          - 'null'
        following:
          type:
          - integer
          - 'null'
        posts_count:
          type:
          - integer
          - 'null'
        verified:
          type:
          - boolean
          - 'null'
        is_private:
          type:
          - boolean
          - 'null'
        external_url:
          type:
          - string
          - 'null'
        url:
          type:
          - string
          - 'null'
    PostsEnvelope:
      type: object
      required:
      - success
      - platform
      - endpoint
      - data
      - error
      - credits_used
      - cached
      - request_id
      description: 'Same envelope as every other route. `data.items` carries the posts the account itself wrote — reposts and boosts of other people are excluded by authorship. `has_more` is per platform, not one answer for all three: Bluesky computes it from the source cursor, Mastodon reports false until we read the `Link` header, and Threads reports null because its posts come inside the profile document and cannot be paged. A null is us saying we do not know, which is different from saying there is no more.'
      properties:
        success:
          type: boolean
        platform:
          type: string
        endpoint:
          type: string
        data:
          type:
          - object
          - 'null'
          properties:
            author:
              $ref: '#/components/schemas/Author'
            items:
              type: array
              items:
                $ref: '#/components/schemas/PostItem'
            has_more:
              type:
              - boolean
              - 'null'
            next_cursor:
              type:
              - string
              - 'null'
        error:
          type:
          - object
          - 'null'
        credits_used:
          type: integer
        cached:
          type: boolean
        request_id:
          type: string
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: 'API key, sent as `Authorization: Bearer hk_live_...`. Quota is monthly and renews on the 1st. Unused quota does not roll over and does not expire mid-cycle — you get the whole month.'
x-provenance:
  method: harvested
  publisher: HonestHook
  source: https://honesthook.com/openapi.json
  note: split by refine-openapis from the provider's own published document, verified by fetch (openapi/_original/*.fetch.json);
    stamped by stamp-openapi-provenance.py (roadmap#793)