SDKs and client libraries
Install and use the official Openhandle SDKs for TypeScript, Go, Python, PHP, and Rust.
Openhandle publishes official TypeScript, JavaScript, Go, Python, PHP, and Rust SDKs. Every API-reference page lets you switch between cURL and every SDK for the same deterministic Test request.
TypeScript SDK
Install @openhandle/sdk:
pnpm add @openhandle/sdkCreate one client and select resources synchronously. Only terminal operations
such as get, list, search, and fetch perform network requests.
import { OpenHandle } from '@openhandle/sdk';
const openhandle = new OpenHandle({ apiKey: process.env.OPENHANDLE_TEST_KEY! });
const profile = openhandle.instagram.profile('northstar_forge_test');
const response = await profile.get();
const posts = await profile.posts.list({ freshness: '24h' });Profile strings are usernames. Use an explicit object for a platform ID or URL:
openhandle.instagram.profile({ id: '25025320' });
openhandle.instagram.profile({ url: 'https://www.instagram.com/openai/' });IDs are opaque strings; numeric ID values are rejected before a request is made.
Named types
The package exports camel-cased models and concise types for every operation. Use them when a wrapper, cache, callback, or public function needs an explicit type annotation:
import type {
InstagramProfile,
InstagramProfilePostsOptions,
InstagramProfilePostsPage,
InstagramProfileResponse,
} from '@openhandle/sdk';
async function getProfile(): Promise<InstagramProfileResponse> {
return openhandle.instagram.profile('northstar_forge_test').get();
}
function readProfile(profile: InstagramProfile) {
return profile.handle;
}Operation names omit terminal get and list words, so
instagram.profile.posts.list maps to InstagramProfilePostsOptions and
InstagramProfilePostsPage.
Pagination
List operations return a typed page. next() is lazy and returns null after
the final page.
let page = await profile.posts.list({ freshness: '24h' });
while (true) {
for (const post of page.data) {
console.log(post.id);
}
const nextPage = await page.next();
if (!nextPage) break;
page = nextPage;
}Each fetched page is one request. See pagination for cursor and upstream-switch rules.
Errors
The SDK retries explicitly retryable failures by default. Catch
OpenHandleError and branch on code, never message.
import { OpenHandleError } from '@openhandle/sdk';
try {
await profile.get();
} catch (error) {
if (error instanceof OpenHandleError) {
console.error(error.code, error.requestId, error.retryable);
}
throw error;
}See envelope and errors for the public error catalog.
Go SDK
Install openhandle-go. The
SDK requires Go 1.27 or newer.
go get github.com/openhandlehq/openhandle-goCreate one client and select resources synchronously. Only terminal operations
such as Get, List, Search, and Fetch perform network requests.
package main
import (
"context"
"fmt"
"log"
"os"
openhandle "github.com/openhandlehq/openhandle-go"
)
func main() {
client, err := openhandle.New(os.Getenv("OPENHANDLE_TEST_KEY"))
if err != nil {
log.Fatal(err)
}
profile := client.Instagram.Profile("northstar_forge_test")
response, err := profile.Get(context.Background(), &openhandle.InstagramProfileOptions{
Freshness: openhandle.FreshnessTwentyFourH,
})
if err != nil {
log.Fatal(err)
}
fmt.Println(response.Data.Handle)
}Raw strings use the selected resource's natural shorthand. Explicit reference types remove ambiguity for IDs and URLs.
client.Instagram.Profile("openai")
client.Instagram.Profile(openhandle.Username("12356"))
client.Instagram.Profile(openhandle.ID("25025320"))
client.Instagram.Profile(openhandle.URL("https://www.instagram.com/openai/"))Pagination
Pages expose the opaque next cursor. Next is lazy and returns nil after the
final page.
page, err := client.Instagram.Profile("northstar_forge_test").Posts.List(
context.Background(),
&openhandle.InstagramProfilePostsOptions{
Freshness: openhandle.FreshnessTwentyFourH,
},
)
for page != nil && err == nil {
for _, post := range page.Data {
fmt.Println(post.ID)
}
page, err = page.Next(context.Background())
}
if err != nil {
log.Fatal(err)
}Each fetched page is one request. See pagination for cursor and upstream-switch rules.
Errors
The SDK retries explicitly retryable failures by default. Use errors.As to
read *openhandle.Error. Branch on Code, never Message.
var apiError *openhandle.Error
if errors.As(err, &apiError) {
log.Printf("code=%s request_id=%s retryable=%t", apiError.Code, apiError.RequestID, apiError.Retryable)
}See envelope and errors for the public error catalog.
Python SDK
Install openhandle. The
package requires Python 3.10 or newer and ships full type annotations.
pip install openhandleCreate one client and select resources synchronously. Only terminal operations
such as get, list, search, and fetch perform network requests.
import os
from openhandle import OpenHandle
openhandle = OpenHandle(api_key=os.environ["OPENHANDLE_TEST_KEY"])
profile = openhandle.instagram.profile("northstar_forge_test")
response = profile.get()
posts = profile.posts.list(freshness="24h")
print(response.data["handle"], len(posts.data))Profile strings are usernames. Use an explicit keyword for a platform ID or URL:
openhandle.instagram.profile(id="25025320")
openhandle.instagram.profile(url="https://www.instagram.com/openai/")IDs are opaque strings; numeric ID values are rejected before a request is made.
Named models
Responses stay typed through the generated models in openhandle.models:
from openhandle import OpenHandle, Response
from openhandle.models import InstagramProfile
def get_profile(openhandle: OpenHandle) -> Response[InstagramProfile]:
return openhandle.instagram.profile("northstar_forge_test").get()Pagination
List operations return a typed page. next() is lazy and returns None after
the final page. items() iterates lazily across pages, one request per page.
page = profile.posts.list(freshness="24h")
while page is not None:
for post in page.data:
print(post["id"])
page = page.next()
for post in profile.posts.items(freshness="24h"):
print(post["id"])Each fetched page is one request. See pagination for cursor and upstream-switch rules.
Async client
AsyncOpenHandle exposes the same resource graph with async terminal
operations:
from openhandle import AsyncOpenHandle
async with AsyncOpenHandle(api_key=os.environ["OPENHANDLE_TEST_KEY"]) as openhandle:
response = await openhandle.instagram.profile("northstar_forge_test").get()
async for post in openhandle.instagram.profile("northstar_forge_test").posts.items():
print(post["id"])Errors
The SDK retries explicitly retryable failures by default. Catch
OpenHandleError and branch on code, never message.
from openhandle import OpenHandleError
try:
profile.get()
except OpenHandleError as error:
print(error.code, error.request_id, error.retryable)
raiseSee envelope and errors for the public error catalog.
PHP SDK
Install openhandle/sdk with
Composer. The package requires PHP 8.2 or newer and uses Guzzle for HTTP.
composer require openhandle/sdkCreate one client and select resources synchronously. Only terminal operations
such as get, list, search, and fetch perform network requests. Pass
options as named arguments.
use OpenHandle\OpenHandle;
$openhandle = new OpenHandle(apiKey: (string) getenv('OPENHANDLE_TEST_KEY'));
$profile = $openhandle->instagram->profile('northstar_forge_test');
$response = $profile->get();
$posts = $profile->posts->list(freshness: '24h');
echo $response->data->handle, ' ', count($posts->data), PHP_EOL;Profile strings are usernames. Use a named argument for a platform ID or URL:
$openhandle->instagram->profile(id: '25025320');
$openhandle->instagram->profile(url: 'https://www.instagram.com/openai/');IDs are opaque strings; numeric ID values are rejected before a request is made.
Named models
Every response has a typed model in the OpenHandle\Models namespace.
Response and Page carry PHPStan generics, so your own functions stay typed:
use OpenHandle\Models\InstagramProfile;
use OpenHandle\OpenHandle;
use OpenHandle\Response;
/**
* @return Response<InstagramProfile>
*/
function getProfile(OpenHandle $openhandle): Response
{
return $openhandle->instagram->profile('northstar_forge_test')->get();
}Pagination
List operations return a typed page. next() is lazy and returns null after
the final page. items() is a generator that fetches one page per request.
for ($page = $profile->posts->list(freshness: '24h'); $page !== null; $page = $page->next()) {
foreach ($page->data as $post) {
echo $post->id, PHP_EOL;
}
}
foreach ($profile->posts->items(freshness: '24h') as $post) {
echo $post->id, PHP_EOL;
}Each fetched page is one request. See pagination for cursor and upstream-switch rules.
Errors
The SDK retries explicitly retryable failures by default. Catch
OpenHandleError and branch on code, never message.
use OpenHandle\OpenHandleError;
try {
$profile->get();
} catch (OpenHandleError $error) {
error_log("{$error->code} {$error->requestId}");
throw $error;
}See envelope and errors for the public error catalog.
Rust SDK
Install openhandle with
Cargo. The crate is async, runs on Tokio, and requires Rust 1.85 or newer.
cargo add openhandle
cargo add tokio --features macros,rt-multi-threadCreate a client, then select a resource such as a profile. Selecting a resource does not send a request.
Methods such as get, list, search, and fetch send requests when you await them.
Pass options to these methods, or use None for the defaults.
use openhandle::{Client, Freshness, InstagramProfilePostsOptions};
#[tokio::main]
async fn main() -> Result<(), openhandle::Error> {
let client = Client::new(std::env::var("OPENHANDLE_TEST_KEY").unwrap_or_default())?;
let profile = client.instagram().profile("northstar_forge_test");
let response = profile.get(None).await?;
let posts = profile
.posts()
.list(InstagramProfilePostsOptions::new().freshness(Freshness::TwentyFourHours))
.await?;
println!("{} {}", response.data.handle, posts.data.len());
Ok(())
}Profile strings are usernames. Use Id or Url for a platform ID or URL:
use openhandle::{Id, Url};
client.instagram().profile(Id::new("25025320"));
client.instagram().profile(Url::new("https://www.instagram.com/openai/"));IDs are strings, so a numeric ID does not compile.
Named models
Every response has a typed model in openhandle::models. Use Response and
Page when your own functions need an explicit type:
use openhandle::models::InstagramProfile;
use openhandle::{Client, Error, Response};
async fn get_profile(client: &Client) -> Result<Response<InstagramProfile>, Error> {
client
.instagram()
.profile("northstar_forge_test")
.get(None)
.await
}Options types follow the operation, so instagram.profile.posts.list takes
InstagramProfilePostsOptions and returns InstagramProfilePostsPage.
Required values, such as the search text q, are constructor arguments:
TikTokSearchPostsOptions::new("synthetic").
Pagination
List operations return a typed page. next() is lazy and returns None after
the final page. items() is a stream that fetches one page per request.
let mut page = profile.posts().list(None).await?;
loop {
for post in &page.data {
println!("{}", post.id);
}
match page.next().await? {
Some(next) => page = next,
None => break,
}
}
let mut posts = profile.posts().items(None);
while let Some(post) = posts.next().await {
println!("{}", post?.id);
}Each fetched page is one request. See pagination for cursor and upstream-switch rules.
Errors
By default, the SDK retries failures marked as retryable.
Match Error::Api and use code to decide how to handle the error. Do not use message for this.
use openhandle::Error;
match profile.get(None).await {
Ok(response) => println!("{}", response.data.handle),
Err(Error::Api(error)) => {
eprintln!("{} {:?} {}", error.code, error.request_id, error.retryable);
}
Err(error) => eprintln!("{error}"),
}Error::Reference and Error::ReferenceMismatch mean a reference was
invalid. No request was made.
See envelope and errors for the public error catalog.
Test and Live
The client does not take an environment option. The API key selects Test or
Live: oh_test_ keys use deterministic synthetic data at a $0.000 actual
charge, while oh_live_ keys use public live data and normal billing. Moving
to Live only requires changing the key and identifiers.