Concepts

Envelope and errors

Every Openhandle response uses one envelope. Every error has a code, a request ID, and a retry rule.

Every successful response wraps its data in the same envelope, and every error returns the same error shape with a machine-readable code and an explicit retry flag. You handle responses once, for every platform and every endpoint.

The success envelope

{
  "platform": "instagram",
  "resource": "profile",
  "capturedAt": "2026-08-06T12:04:11Z",
  "source": "live",
  "data": { "…": "…" },
  "meta": {
    "cursors": { "next": "opaque-cursor" }
  }
}
FieldMeaning
platforminstagram, tiktok, twitter, or reddit
resourceprofile, post, comment, hashtag, location, music, category, list, or normalized extended entity. Reddit adds subreddit, rule, wiki, and trophy
capturedAtRFC 3339 timestamp of when the data was captured
sourcelive (fetched now) or cache (served from an earlier capture)
dataThe normalized resource, or an array of resources for lists
metaAlways on lists, with cursors.next and any page metadata. See pagination. A single object has meta only when the platform sends extra metadata. Its cursors.next is then always null

Shared cores and platform schemas

Openhandle reuses schemas only when their meaning is stable across platforms. For example, every post composes the shared PostCore, PostMetrics, PostAuthor, PostMedia, and MediaReference components. The final response is still an InstagramPost, TikTokPost, or TwitterPost, with a matching platform metrics schema and platform-only relationships.

Check each endpoint schema for field presence and nullability. Measured zeroes remain 0. Instagram profiles, comments, hashtags, and locations omit missing fields. Platform fields remain useful when another platform lacks an equivalent. TikTok can return metrics.downloads, while Twitter can return metrics.quotes, metrics.bookmarks, quoted_post, and reposted_post. Media keeps a preferred url for simple clients, lists mirror addresses of the same file in urls, and exposes every usable public encoding in renditions when the upstream response supplies alternatives.

Each endpoint owns its response schema. Shared names express shared meanings. Every platform uses named groups such as metrics, verification, location, and settings, without an extended wrapper. Each endpoint publishes a fixed key set. A field the provider did not supply is null. Supplied false, zero, empty text, and empty arrays remain as supplied. The answered object promises its identifier, URL, profile handle, post timestamp, and media array; those are never null. The same type reached through another object, such as a post author or a quoted post, is a Reference schema whose identifying fields may be null when the provider left them out. Domain-specific endpoints use dedicated schemas where the provider data is stable enough, for example InstagramStory, InstagramHighlight, TikTokPlaylist, and TikTokEffect. Heterogeneous search endpoints use discriminated search-result nodes, and filter/type endpoints use typed option nodes. No published operation falls back to the generic Entity schema. See the generated OpenAPI document for the exact schema attached to each operation.

The error envelope

{
  "error": {
    "code": "PROFILE_NOT_FOUND",
    "message": "This profile was not found.",
    "requestId": "req_01j8…",
    "retryable": false
  }
}

Always branch on code, not on message. Include requestId when you contact support. When retryable is true, retry with backoff; when it is false, retrying will not help.

Error catalog

CodeStatusRetryableMeaning
UNAUTHENTICATED401NoA valid API key is required
EMAIL_VERIFICATION_REQUIRED403NoVerify the workspace-owner email before using the API
BALANCE_EMPTY402NoAdd balance to continue
BILLING_SUSPENDED402NoBilling for this workspace is suspended
SECURITY_SUSPENDED403NoAPI access for this workspace is suspended
PROFILE_NOT_FOUND404NoThe profile does not exist
PROFILE_PRIVATE403NoThe profile is private
PROFILE_BLOCKED451NoThe owner of this profile asked us not to serve it. Posts and comments by this profile are left out of lists
POST_NOT_FOUND404NoThe post does not exist
COMMENT_NOT_FOUND404NoThe comment does not exist
RESOURCE_NOT_FOUND404NoThe hashtag, location, music, playlist, subreddit, or other resource does not exist
INVALID_IDENTIFIER400NoThe identifier is invalid, or this endpoint cannot read it
INVALID_URL400NoThe social URL is invalid or unsupported
INVALID_QUERY400NoA search query is required. Send it as q
INVALID_FRESHNESS400NoFreshness must be live, 24h, 7d, or 30d
INVALID_SINCE400Nosince must be an RFC 3339 timestamp
INVALID_PARAMETER400NoA query parameter such as sort or type has a value the endpoint does not accept
CURSOR_MISMATCH400NoThe cursor does not belong to this request
UPSTREAM_SWITCHED409NoThe upstream source no longer accepts this cursor. Restart pagination from the first page
RATE_LIMITED429YesRate limit exceeded. Honor the Retry-After header
SERVICE_UNAVAILABLE503YesThe service is temporarily unavailable
UPSTREAM_DEGRADED503YesThe upstream platform is temporarily unavailable
INTERNAL_ERROR500YesThe request could not be completed
ROUTE_NOT_FOUND404NoThe API route does not exist
METHOD_NOT_ALLOWED405NoThe HTTP method is not allowed on this route

Billing and errors

You pay for answers, not attempts. PROFILE_NOT_FOUND, PROFILE_PRIVATE, and other definitive answers are billable: the API did the work and gave you a usable result. Provider failures and internal errors are never charged. PROFILE_BLOCKED is never charged either.

On this page