Skip to content
View raw

Hyperindex

Hyperindex is the hosted GraphQL read API for Hypercerts and Certified AT Protocol records. Use it when you want to build applications, profile pages, discovery views, dashboards, analytics, or curated experiences over indexed Hypercerts data.

Hyperindex does not replace AT Protocol repositories. Records are created, updated, and deleted in users' repositories. Hyperindex indexes those records and exposes a queryable read model.

Endpoints#

EnvironmentGraphQL endpointPlayground
Productionhttps://api.indexer.hypercerts.dev/graphqlhttps://api.indexer.hypercerts.dev/graphiql
Staginghttps://dev.api.indexer.hypercerts.dev/graphqlhttps://dev.api.indexer.hypercerts.dev/graphiql

Use production for normal application traffic. Production and staging both index data from the same AT Protocol network. Staging is mainly for earlier Hyperindex API, schema, and indexing features; it may expose new functionality before production does.

Public GraphQL queries do not require an API key.

What Hyperindex gives you#

Hyperindex turns AT Protocol records into a GraphQL API with:

  • typed queries for Hypercerts and Certified collections
  • field-level selection, filtering, sorting, and pagination
  • record metadata such as uri, cid, did, and rkey
  • generic JSON access for lower-level workflows
  • text search over indexed record JSON

Architecture#

At a high level, Hyperindex is a read-side indexer and GraphQL API.

text
AT Protocol repositories

        │ repo commits and record events

AT Protocol Relay

        │ network event stream

Tap / AT Protocol ingestion

        │ normalized records for indexed collections

Hyperindex record store

        │ dynamically generated GraphQL schema

Public GraphQL API

Source of truth#

The source of truth is the AT Protocol repository that owns a record. Hyperindex stores an indexed copy so consumers can query across records efficiently.

Use these identifiers carefully:

FieldMeaningWhen to use it
uriThe AT-URI of a record, usually at://<did>/<collection>/<rkey>Stable record identity and links between records
cidThe CID of the indexed record versionVersion-sensitive reads and cache validation
authorStructured author identity with non-null did and nullable current handleRendering and linking the account that owns the record
didDeprecated direct DID output; use author.did insteadCompatibility with existing consumers; where.did filters remain supported
rkeyThe record key, the last segment of the AT-URILow-level AT Protocol workflows
createdAtTimestamp declared inside the recordUser-facing chronology
indexed_atHyperindex arrival orderIndexer-facing chronology

If you only need to link to a logical record, store the uri. If your application cares about the exact content version, store both uri and cid.

Schema generation#

Hyperindex dynamically builds its public GraphQL schema from AT Protocol Lexicons. A collection NSID becomes a typed GraphQL query:

Collection NSIDList querySingle-record query
org.hypercerts.claim.activityorgHypercertsClaimActivityorgHypercertsClaimActivityByUri
org.hypercerts.collectionorgHypercertsCollectionorgHypercertsCollectionByUri
org.hypercerts.context.attachmentorgHypercertsContextAttachmentorgHypercertsContextAttachmentByUri
app.certified.actor.profileappCertifiedActorProfileappCertifiedActorProfileByUri
app.certified.link.evmappCertifiedLinkEvmappCertifiedLinkEvmByUri

Use typed queries first. They provide typed fields, filters, sorting, and pagination. Typed collection queries only expose records that Indigo has validated against the startup Lexicon set used to generate the running schema. If an observed record is malformed for that saved schema, or if Hyperindex has no saved Lexicon for its collection, it is hidden from typed collection list queries, typed ByUri queries, typed counts, relationship hydration, and typed create/update subscriptions. Typed delete subscriptions emit only when the deleted row was valid before deletion.

Use the generic records(collection: ...) query or search(...) when you need raw JSON, debugging visibility, or access to records hidden from typed GraphQL. Hyperindex stores every observed record in the raw record table even when validation fails or no saved Lexicon is available. Generic record and search results include validation metadata:

FieldMeaning
validationStatusvalid, invalid, unknown_schema, or validation_error
validationErrorExplanation of why the record is hidden from typed GraphQL, when available
validatedAtTimestamp of the most recent local validation classification
lexiconHashSHA-256 validation fingerprint for the saved collection Lexicon and any transitive referenced Lexicons used for classification

Validation is local-only. During normal ingestion Hyperindex uses Indigo to validate against the Lexicons loaded at startup and does not resolve _lexicon DNS records, DID documents, PDS-hosted schema records, or other remote schema sources.

Public typed GraphQL, record validation, startup record refresh, and default Jetstream collection filters use one fixed Lexicon set loaded at startup. Uploading, registering, or deleting a Lexicon changes only the saved configuration; restart or redeploy Hyperindex to apply the change to all of those runtime surfaces together. In a multi-replica deployment, coordinate a Lexicon-changing rollout so old-snapshot and new-snapshot backend replicas never serve concurrently against the shared validation metadata.

The generic recordEvents subscription receives all observed raw create/update/delete events, including events for invalid or unknown-schema records. Typed collection subscriptions filter that stream to valid create/update rows and deletes that were valid before removal.

All generated record types, generic record results, timeline nodes, and record subscription payloads expose:

graphql
author {
  did
  handle
}

author is non-null, while author.handle is nullable when no current verified handle is available. The direct record-level did field is deprecated but remains functional during the transition to author.did. This output-field deprecation does not affect where.did filters.

author is reserved metadata on generated record types. Lexicons that define their own top-level author property must rename it because that property and its generated filter are skipped in favor of ActorIdentity.

Relationships between records#

Hypercert records often reference other records with AT Protocol strong refs, usually { uri, cid }. Hyperindex exposes those refs, but it does not automatically join every referenced record.

For example:

  • a hypercert can reference a rights record
  • an attachment can reference one or more subject records
  • a collection can reference activity claims or other collections
  • a badge award can reference a badge definition and a subject

For arbitrary references, read the referenced uri and fetch it with the matching ByUri query, or use search when you need to find records that mention a nested AT-URI.

Consistency model#

Hyperindex is eventually consistent with the AT Protocol network. Indexing is generally fast, but relay or Tap hiccups can occasionally make indexing slower, so consumers should design for this by:

  • using pagination instead of assuming fixed result sets
  • storing uri for stable record references
  • storing cid when exact record versions matter
  • retrying recent writes before treating missing records as permanent

Core collections#

The hosted Hypercerts indexer exposes typed queries for these main collections.

ConceptCollectionQuery
Hypercert activity claimsorg.hypercerts.claim.activityorgHypercertsClaimActivity
Contribution detailsorg.hypercerts.claim.contributionorgHypercertsClaimContribution
Contributor informationorg.hypercerts.claim.contributorInformationorgHypercertsClaimContributorInformation
Rights and transfer termsorg.hypercerts.claim.rightsorgHypercertsClaimRights
Collections of hypercertsorg.hypercerts.collectionorgHypercertsCollection
Acknowledgementsorg.hypercerts.context.acknowledgementorgHypercertsContextAcknowledgement
Attachments and evidenceorg.hypercerts.context.attachmentorgHypercertsContextAttachment
Evaluationsorg.hypercerts.context.evaluationorgHypercertsContextEvaluation
Measurementsorg.hypercerts.context.measurementorgHypercertsContextMeasurement
Funding receiptsorg.hypercerts.funding.receiptorgHypercertsFundingReceipt
Work-scope tagsorg.hypercerts.workscope.tagorgHypercertsWorkscopeTag
Certified profilesapp.certified.actor.profileappCertifiedActorProfile
Certified organizationsapp.certified.actor.organizationappCertifiedActorOrganization
Certified followsapp.certified.graph.followappCertifiedGraphFollow
EVM wallet linksapp.certified.link.evmappCertifiedLinkEvm
Locationsapp.certified.locationappCertifiedLocation
Badge definitionsapp.certified.badge.definitionappCertifiedBadgeDefinition
Badge awardsapp.certified.badge.awardappCertifiedBadgeAward
Badge responsesapp.certified.badge.responseappCertifiedBadgeResponse

Query model#

Most typed list queries accept:

  • first and after for forward pagination
  • last and before for backward pagination
  • where for filtering
  • sortBy and sortDirection for ordering

Pagination defaults to 20 records. The maximum page size is 1000 records.

Common filters:

graphql
where: { uri: { eq: "at://did:plc:example/org.hypercerts.claim.activity/rkey" } }
where: { uri: { in: ["at://did:plc:example/org.hypercerts.claim.activity/rkey1", "at://did:plc:example/org.hypercerts.claim.activity/rkey2"] } }
where: { did: { eq: "did:plc:example" } }
where: { title: { contains: "reforestation" } }
where: { createdAt: { gte: "2026-01-01T00:00:00Z" } }
where: { image: { isNull: false } }

The generated uri filter is a record metadata filter for exact AT-URI lookup and batched hydration. It supports eq and in and does not search the JSON payload.

Any single in operator accepts up to 100 values. For larger DID, URI, label source/value, or scalar batches, split the values into multiple GraphQL requests and merge the paginated results client-side. This limit is separate from connection page size.

Scalar fields support value filters such as eq, neq, in, contains, startsWith, gt, lt, gte, lte, and isNull, depending on the scalar type.

Complex fields support presence checks with isNull. Some complex fields use the shared PresenceFilterInput; arrays, refs, and unions may instead expose generated nested filter inputs that also include isNull. Do not rely on the input type name for presence checks; introspect the field and use isNull. Nested scalar leaves support exact operators only: eq, in, and isNull. Use array any when at least one array item should match; multiple predicates inside the same any must match the same array item. Nested array fields inside an existing any scope expose presence checks only; Hyperindex does not advertise nested any within another any.

graphql
where: {
  image: { isNull: false }
  contributors: {
    any: {
      contributorIdentity: { identity: { eq: "did:plc:example" } }
    }
  }
}

Nested filters do not support substring operators (contains, startsWith), comparison operators (gt, lt, gte, lte), nested-array any filters inside another any, nested sorting, arbitrary JSON paths, or automatic strong-ref dereferencing. A small set of explicit collection filter extensions may perform product-specific cross-record lookups; uploaded lexicons do not get these fields automatically.

For Hypercerts activity contributors that may be inline, legacy bare DID strings, or org.hypercerts.claim.contributorInformation strong refs, use the compatibility filter:

graphql
where: { contributorDid: { eq: "did:plc:example" } }

For Certified badge awards, use badgeType to filter by the referenced app.certified.badge.definition.badgeType without joining badge definitions client-side:

graphql
where: { badgeType: { eq: "endorsement" } }

badgeType uses StringFilterInput, so it supports the same string operators exposed for badge definitions. Awards whose referenced badge definition is missing or has no badgeType do not match positive value filters.

Certified endorsement closure#

Use endorsementClosure(where, first, after) when a client needs a DID-rooted Certified endorsement network: accounts directly or indirectly reached through active endorsement badge awards.

graphql
query EndorsementClosure($did: String!) {
  endorsementClosure(
    where: { did: { eq: $did } }
    first: 100
  ) {
    truncated
    totalCount
    pageInfo { hasNextPage endCursor }
    edges {
      cursor
      node {
        did
        degree
        certifiedProfileData { did displayName avatar }
        viaAccounts {
          did
          certifiedProfileData { did displayName avatar }
        }
      }
    }
  }
}

Variables:

JSON
{ "did": "did:plc:example" }

where.did.eq is required and selects the root DID. The endorsement closure DID filter exposes only eq, not in, because each request is rooted at one DID. Optional where.degree.eq returns only one hop distance; the value must be 1, 2, or 3. Omit where.degree to return all supported degrees. Results are sorted by degree then DID. certifiedProfileData resolves the reached account's Certified profile when one exists. viaAccounts lists up to 64 previous-ring accounts that led to an account, including each predecessor DID and optional Certified profile data; it is empty for degree-1 accounts. truncated is true when the server-side account cap is reached; clients should treat the response as a useful subset, not a complete network.

The resolver computes edges from current app.certified.badge.award, app.certified.badge.definition, and app.certified.badge.response records at request time. Only records whose validationStatus is valid participate, so invalid awards or definitions cannot create edges and invalid responses cannot suppress them. An active edge requires an endorsement-typed badge definition, an app.certified.defs#did account subject with a valid DID, an issuer allowed by allowedIssuers when that badge definition has an allowlist, no self-loop, and no rejection response authored by the subject for that award. Badge awards to record strongRefs do not create account endorsement edges.

External and author label filters#

Hyperindex can filter typed record connections by locally ingested external ATProto labels before pagination and totalCount are calculated.

Use where.externalLabels when the label subject is the record AT-URI:

graphql
where: {
  externalLabels: {
    has: {
      src: { eq: "did:plc:labeler" }
      val: { eq: "high-quality" }
      activeOnly: true
    }
  }
}

Use where.authorLabels when the label subject is the record author's account DID:

graphql
where: {
  authorLabels: {
    none: {
      src: { eq: "did:plc:pswneepkd5lesumj7ejmkbal" }
      val: { eq: "likely-test" }
      activeOnly: true
    }
  }
}

authorLabels only matches DID-subject labels such as uri: "did:plc:..." with no CID. It does not infer account labels from profile or organization records. Unlabeled authors pass none predicates and fail has predicates.

To require certified authors, use has with multiple values:

graphql
where: {
  authorLabels: {
    has: {
      src: { eq: "did:plc:pswneepkd5lesumj7ejmkbal" }
      val: { in: ["standard", "high-quality"] }
      activeOnly: true
    }
  }
}

There is no node-level authorLabels field. To display account labels for known DIDs, use the root externalLabels(subjects: [...]) query with those DID subjects.

Quickstart#

Send a POST request with a GraphQL query and optional variables.

Terminal
curl -s https://api.indexer.hypercerts.dev/graphql \
  -H 'content-type: application/json' \
  --data '{"query":"query { orgHypercertsClaimActivity(first: 1) { edges { node { uri title } } } }"}'

A minimal TypeScript helper:

ts
const endpoint = "https://api.indexer.hypercerts.dev/graphql";


export async function queryHyperindex<T>(
  query: string,
  variables?: Record<string, unknown>,
): Promise<T> {
  const response = await fetch(endpoint, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ query, variables }),
  });


  if (!response.ok) {
    throw new Error(`Hyperindex request failed: ${response.status} ${response.statusText}`);
  }


  const payload = await response.json();


  if (payload.errors?.length) {
    throw new Error(payload.errors.map((error: { message: string }) => error.message).join("\n"));
  }


  return payload.data as T;
}

Example: query recent hypercerts#

graphql
query RecentHypercerts($after: String) {
  orgHypercertsClaimActivity(
    first: 20
    after: $after
    sortBy: createdAt
    sortDirection: DESC
  ) {
    edges {
      cursor
      node {
        uri
        cid
        did
        title
        shortDescription
        createdAt
      }
    }
    pageInfo { hasNextPage endCursor }
  }
}

Variables:

JSON
{ "after": null }

Example: fetch a hypercert by AT-URI#

graphql
query HypercertByUri($uri: String!) {
  orgHypercertsClaimActivityByUri(uri: $uri) {
    uri
    cid
    did
    title
    shortDescription
    createdAt
    startDate
    endDate
    rights { uri cid }
    image {
      __typename
      ... on OrgHypercertsDefsUri { uri }
      ... on OrgHypercertsDefsSmallImage {
        image { ref mimeType size }
      }
    }
  }
}

Variables:

JSON
{
  "uri": "at://did:plc:example/org.hypercerts.claim.activity/example-rkey"
}

Example: search record JSON#

Use search for simple discovery, substring matching, or nested AT-URI references that are not covered by generated exact nested filters.

graphql
query SearchAttachments($hypercertUri: String!, $after: String) {
  search(
    query: $hypercertUri
    collection: "org.hypercerts.context.attachment"
    first: 20
    after: $after
  ) {
    edges {
      cursor
      node {
        uri
        cid
        did
        collection
        value
      }
    }
    pageInfo { hasNextPage endCursor }
  }
}

Variables:

JSON
{
  "hypercertUri": "at://did:plc:example/org.hypercerts.claim.activity/example-rkey",
  "after": null
}

Working with unions, refs, and blobs#

For unions, request __typename and use inline fragments:

graphql
image {
  __typename
  ... on OrgHypercertsDefsUri { uri }
  ... on OrgHypercertsDefsSmallImage {
    image { ref mimeType size }
  }
}

For strong references, request both uri and cid:

graphql
rights { uri cid }

For blobs, request the blob reference and metadata:

graphql
image { ref mimeType size }

Service health endpoints#

In addition to GraphQL, hosted Hyperindex exposes lightweight status endpoints:

EndpointMeaning
/healthLiveness check. Use this to check whether the process is running.
/readyReadiness check. Use this to check whether the API is ready to serve traffic.
/statsPublic operational stats and diagnostics for the indexer.

Best practices#

  • Prefer typed queries over generic JSON queries.
  • Always paginate list queries.
  • Keep selection sets small.
  • Use uri for stable record identity.
  • Use uri plus cid for version-sensitive data.
  • Use generated nested filters for exact nested matches when available; use search or client-side filtering for substring matching or unsupported nested shapes.
  • Request totalCount only when your UI needs it.

Troubleshooting#

Cannot query field ...#

The selected endpoint's schema does not expose that field. Check that you are using the right environment and inspect the schema in GraphiQL.

A nested filter does not work#

Generated nested filters only cover arrays, refs, and unions up to three lexicon path segments deep, and nested scalar leaves only support eq, in, and isNull. Multiple predicates inside the same array any are evaluated against the same array item. Nested array fields inside an existing any scope expose presence checks only, not another any. They do not support substring operators (contains, startsWith), comparison operators (gt, lt, gte, lte), arbitrary JSON paths, nested sorting, or automatic strong-ref dereferencing. Introspect the target endpoint's WhereInput; if the nested input is absent, use search, follow a referenced uri, or filter client-side.

A recently written record is missing#

Hyperindex is generally fast, but it is still eventually consistent with AT Protocol repositories. Retry after a short delay and confirm the record belongs to an indexed collection.