API walkthrough

The Braintrust REST API is available via an OpenAPI spec published at https://github.com/braintrustdata/braintrust-openapi. This guide walks through a few common use cases, and should help you get started with using the API. Each example is implemented in a particular language, for legibility, but the API itself is language-agnostic.

To learn more about the API, see the full API spec. If you are looking for a language-specific wrapper over the bare REST API, we support several different languages.

Running an experiment

import os
from uuid import uuid4
 
import requests
 
API_URL = "https://api.braintrust.dev/v1"
headers = {"Authorization": "Bearer " + os.environ["BRAINTRUST_API_KEY"]}
 
if __name__ == "__main__":
    # Create a project, if it does not already exist
    project = requests.post(f"{API_URL}/project", headers=headers, json={"name": "rest_test"}).json()
    print(project)
 
    # Create an experiment. This should always be new
    experiment = requests.post(
        f"{API_URL}/experiment", headers=headers, json={"name": "rest_test", "project_id": project["id"]}
    ).json()
    print(experiment)
 
    # Log some stuff
    for i in range(10):
        resp = requests.post(
            f"{API_URL}/experiment/{experiment['id']}/insert",
            headers=headers,
            json={"events": [{"id": uuid4().hex, "input": 1, "output": 2, "scores": {"accuracy": 0.5}}]},
        )
        if not resp.ok:
            raise Exception(f"Error: {resp.status_code} {resp.text}: {resp.content}")

Fetching experiment results

Let's say you have a human review workflow and you want to determine if an experiment has been fully reviewed. You can do this by running a Braintrust query language (BTQL) query:

from: experiment('<experiment_id>')
measures: sum("My review score" IS NOT NULL) AS reviewed, count(1) AS total
filter: span_id = root_span_id -- Only count traces, not spans

To do this in Python, you can use the btql endpoint:

import os
 
import requests
 
API_URL = "https://api.braintrust.dev/"
headers = {"Authorization": "Bearer " + os.environ["BRAINTRUST_API_KEY"]}
 
 
def make_query(experiment_id: str) -> str:
    # Replace "response quality" with the name of your review score column
    return f"""
from: experiment('{experiment_id}')
measures: sum(scores."response quality" IS NOT NULL) AS reviewed, sum(span_id=root_span_id) AS total
"""
 
 
def fetch_experiment_review_status(experiment_id: str) -> dict:
    return requests.post(
        f"{API_URL}/btql",
        headers=headers,
        json={"query": make_query(experiment_id), "fmt": "json"},
    ).json()
 
 
EXPERIMENT_ID = "bdec1c5e-8c00-4033-84f0-4e3aa522ecaf"  # Replace with your experiment ID
print(fetch_experiment_review_status(EXPERIMENT_ID))

Paginating a large dataset

// If you're self-hosting Braintrust, then use your stack's Universal API URL, e.g.
//   https://dfwhllz61x709.cloudfront.net
export const BRAINTRUST_API_URL = "https://api.braintrust.dev";
export const API_KEY = process.env.BRAINTRUST_API_KEY;
 
export async function* paginateDataset(args: {
  project: string;
  dataset: string;
  version?: string;
  // Number of rows to fetch per request. You can adjust this to be a lower number
  // if your rows are very large (e.g. several MB each).
  perRequestLimit?: number;
}) {
  const { project, dataset, version, perRequestLimit } = args;
  const headers = {
    Accept: "application/json",
    "Accept-Encoding": "gzip",
    Authorization: `Bearer ${API_KEY}`,
  };
  const fullURL = `${BRAINTRUST_API_URL}/v1/dataset?project_name=${encodeURIComponent(
    project,
  )}&dataset_name=${encodeURIComponent(dataset)}`;
  const ds = await fetch(fullURL, {
    method: "GET",
    headers,
  });
  if (!ds.ok) {
    throw new Error(
      `Error fetching dataset metadata: ${ds.status}: ${await ds.text()}`,
    );
  }
  const dsJSON = await ds.json();
  const dsMetadata = dsJSON.objects[0];
  if (!dsMetadata?.id) {
    throw new Error(`Dataset not found: ${project}/${dataset}`);
  }
 
  let cursor: string | null = null;
  while (true) {
    const body: string = JSON.stringify({
      query: {
        from: {
          op: "function",
          name: { op: "ident", name: ["dataset"] },
          args: [{ op: "literal", value: dsMetadata.id }],
        },
        select: [{ op: "star" }],
        limit: perRequestLimit,
        cursor,
      },
      fmt: "jsonl",
      version,
    });
    const response = await fetch(`${BRAINTRUST_API_URL}/btql`, {
      method: "POST",
      headers,
      body,
    });
    if (!response.ok) {
      throw new Error(
        `Error fetching rows for ${dataset}: ${
          response.status
        }: ${await response.text()}`,
      );
    }
 
    cursor =
      response.headers.get("x-bt-cursor") ??
      response.headers.get("x-amz-meta-bt-cursor");
 
    // Parse jsonl line-by-line
    const allRows = await response.text();
    const rows = allRows.split("\n");
    let rowCount = 0;
    for (const row of rows) {
      if (!row.trim()) {
        continue;
      }
      yield JSON.parse(row);
      rowCount++;
    }
 
    if (rowCount === 0) {
      break;
    }
  }
}
 
async function main() {
  for await (const row of paginateDataset({
    project: "Your project name", // Replace with your project name
    dataset: "Your dataset name", // Replace with your dataset name
    perRequestLimit: 100,
  })) {
    console.log(row);
  }
}
 
main();

Impersonating a user for a request

User impersonation allows a privileged user to perform an operation on behalf of another user, using the impersonated user's identity and permissions. For example, a proxy service may wish to forward requests coming in from individual users to Braintrust without requiring each user to directly specify Braintrust credentials. The privileged service can initiate the request with its own credentials and impersonate the user so that Braintrust runs the operation with the user's permissions.

To this end, all API requests accept a header x-bt-impersonate-user, which you can set to the ID or email of the user to impersonate. Currently impersonating another user requires that the requesting user has specifically been granted the owner role over all organizations that the impersonated user belongs to. This check guarantees the requesting user has at least the set of permissions that the impersonated user has.

Consider the following code example for configuring ACLs and running a request with user impersonation.

// If you're self-hosting Braintrust, then use your stack's Universal API URL, e.g.
//   https://dfwhllz61x709.cloudfront.net
export const BRAINTRUST_API_URL = "https://api.braintrust.dev";
export const API_KEY = process.env.BRAINTRUST_API_KEY;
 
async function getOwnerRoleId() {
  const roleResp = await fetch(
    `${BRAINTRUST_API_URL}/v1/role?${new URLSearchParams({ role_name: "owner" })}`,
    method: "GET",
    headers: {
        Authorization: `Bearer ${API_KEY}`,
    }
  );
  if (!roleResp.ok) {
    throw new Error(await roleResp.text());
  }
  const roles = await roleResp.json();
  return roles.objects[0].id;
}
 
async function getUserOrgInfo(orgName: string): { user_id: string; org_id: string } {
  const meResp = await fetch(
    `${BRAINTRUST_API_URL}/api/self/me`,
    method: "POST",
    headers: {
        Authorization: `Bearer ${API_KEY}`,
    },
  );
  if (!meResp.ok) {
    throw new Error(await meResp.text());
  }
  const meInfo = await meResp.json();
  orgInfo = meInfo.organizations.find((x) => x.name === orgName);
  if (!orgInfo) {
    throw new Error(`No organization found with name ${orgName}`);
  }
  return { user_id: meInfo.id, org_id: orgInfo.id };
}
 
async function grantOwnershipRole(orgName: string) {
  const ownerRoleId = await getOwnerRoleId();
  const { user_id, org_id } = await getUserOrgInfo(orgName);
 
  // Grant an 'owner' ACL to the requesting user on the organization. Granting
  // this ACL requires the user to have `create_acls` permission on the org, which
  // means they must already be an owner of the org indirectly.
  const aclResp = await fetch(
    `${BRAINTRUST_API_URL}/v1/acl`,
    method: "POST",
    headers: {
        Authorization: `Bearer ${API_KEY}`,
        "Content-Type": "application/json",
    },
    body: JSON.stringify({
        object_type: "organization",
        object_id: org_id,
        user_id,
        role_id: ownerRoleId,
    }),
  );
  if (!aclResp.ok) {
    throw new Error(await aclResp.text());
  }
}
 
async function main() {
  // This only needs to be done once.
  await grantOwnershipRole(process.env.ORG_NAME);
 
  // This will only succeed if the user being impersonated has permissions to
  // create a project within the org.
  const projectResp = await fetch(
    `${BRAINTRUST_API_URL}/v1/project`,
    method: POST,
    headers: {
        Authorization: `Bearer ${API_KEY}`,
        "Content-Type": "application/json",
        "x-bt-impersonate-user": process.env.USER_EMAIL,
    },
    body: JSON.stringify({
        name: "my-project",
        org_name: process.env.ORG_NAME,
    }),
  );
  if (!projectResp.ok) {
    throw new Error(await projectResp.text());
  }
  console.log(await projectResp.json());
}

Postman

Postman is a popular tool for interacting with HTTP APIs. You can load Braintrust's API spec into Postman by simply importing the OpenAPI spec's URL

https://raw.githubusercontent.com/braintrustdata/braintrust-openapi/main/openapi/spec.json

Postman

Tracing with the REST API SDKs

In this section, we demonstrate the basics of logging with tracing using the language-specific REST API SDKs. The end result of running each example should be a single log entry in a project called tracing_test, which looks like the following:

Tracing Test Screenshot

package main
 
import (
	"context"
	"github.com/braintrustdata/braintrust-go"
    "github.com/google/uuid"
    "time"
)
 
type LLMInteraction struct {
    input interface{}
    output interface{}
}
 
func runInteraction0(input interface{}) LLMInteraction {
    return LLMInteraction{
        input: input,
        output: "output0",
    }
}
 
func runInteraction1(input interface{}) LLMInteraction {
    return LLMInteraction{
        input: input,
        output: "output1",
    }
}
 
func getCurrentTime() float64 {
  return float64(time.Now().UnixMilli()) / 1000.
}
 
func main() {
	client := braintrust.NewClient()
 
	// Create a project, if it does not already exist
	project, err := client.Project.New(context.TODO(), braintrust.ProjectNewParams{
		Name: braintrust.F("tracing_test"),
	})
	if err != nil {
		panic(err)
	}
 
    rootSpanId := uuid.NewString()
    client.Project.Logs.Insert(
        context.TODO(),
        project.ID,
        braintrust.ProjectLogInsertParams{
            Events: braintrust.F([]braintrust.ProjectLogInsertParamsEvent{
                braintrust.ProjectLogInsertParamsEventsInsertProjectLogsEventReplace{
                    ID: braintrust.F(rootSpanId),
                    Metadata: braintrust.F(map[string]interface{}{
                        "user_id": "user123",
                    }),
                    SpanAttributes: braintrust.F(braintrust.ProjectLogInsertParamsEventsInsertProjectLogsEventReplaceSpanAttributes{
                        Name: braintrust.F("User Interaction"),
                    }),
                    Metrics: braintrust.F(braintrust.ProjectLogInsertParamsEventsInsertProjectLogsEventReplaceMetrics{
                        Start: braintrust.F(getCurrentTime()),
                    }),
                },
            }),
        },
    )
 
    interaction0Id := uuid.NewString()
    client.Project.Logs.Insert(
        context.TODO(),
        project.ID,
        braintrust.ProjectLogInsertParams{
            Events: braintrust.F([]braintrust.ProjectLogInsertParamsEvent{
                braintrust.ProjectLogInsertParamsEventsInsertProjectLogsEventReplace{
                    ID: braintrust.F(interaction0Id),
                    ParentID: braintrust.F(rootSpanId),
                    SpanAttributes: braintrust.F(braintrust.ProjectLogInsertParamsEventsInsertProjectLogsEventReplaceSpanAttributes{
                        Name: braintrust.F("Interaction 0"),
                    }),
                    Metrics: braintrust.F(braintrust.ProjectLogInsertParamsEventsInsertProjectLogsEventReplaceMetrics{
                        Start: braintrust.F(getCurrentTime()),
                    }),
                },
            }),
        },
    )
    interaction0 := runInteraction0("hello world")
    client.Project.Logs.Insert(
        context.TODO(),
        project.ID,
        braintrust.ProjectLogInsertParams{
            Events: braintrust.F([]braintrust.ProjectLogInsertParamsEvent{
                braintrust.ProjectLogInsertParamsEventsInsertProjectLogsEventMerge{
                    ID: braintrust.F(interaction0Id),
                    IsMerge: braintrust.F(true),
                    Input: braintrust.F(interaction0.input),
                    Output: braintrust.F(interaction0.output),
                    Metrics: braintrust.F(braintrust.ProjectLogInsertParamsEventsInsertProjectLogsEventMergeMetrics{
                        End: braintrust.F(getCurrentTime()),
                    }),
                },
            }),
        },
    )
 
    interaction1Id := uuid.NewString()
    client.Project.Logs.Insert(
        context.TODO(),
        project.ID,
        braintrust.ProjectLogInsertParams{
            Events: braintrust.F([]braintrust.ProjectLogInsertParamsEvent{
                braintrust.ProjectLogInsertParamsEventsInsertProjectLogsEventReplace{
                    ID: braintrust.F(interaction1Id),
                    ParentID: braintrust.F(rootSpanId),
                    SpanAttributes: braintrust.F(braintrust.ProjectLogInsertParamsEventsInsertProjectLogsEventReplaceSpanAttributes{
                        Name: braintrust.F("Interaction 1"),
                    }),
                    Metrics: braintrust.F(braintrust.ProjectLogInsertParamsEventsInsertProjectLogsEventReplaceMetrics{
                        Start: braintrust.F(getCurrentTime()),
                    }),
                },
            }),
        },
    )
    interaction1 := runInteraction1(interaction0.output)
    client.Project.Logs.Insert(
        context.TODO(),
        project.ID,
        braintrust.ProjectLogInsertParams{
            Events: braintrust.F([]braintrust.ProjectLogInsertParamsEvent{
                braintrust.ProjectLogInsertParamsEventsInsertProjectLogsEventMerge{
                    ID: braintrust.F(interaction1Id),
                    IsMerge: braintrust.F(true),
                    Input: braintrust.F(interaction1.input),
                    Output: braintrust.F(interaction1.output),
                    Metrics: braintrust.F(braintrust.ProjectLogInsertParamsEventsInsertProjectLogsEventMergeMetrics{
                        End: braintrust.F(getCurrentTime()),
                    }),
                },
            }),
        },
    )
 
    client.Project.Logs.Insert(
        context.TODO(),
        project.ID,
        braintrust.ProjectLogInsertParams{
            Events: braintrust.F([]braintrust.ProjectLogInsertParamsEvent{
                braintrust.ProjectLogInsertParamsEventsInsertProjectLogsEventMerge{
                    ID: braintrust.F(rootSpanId),
                    IsMerge: braintrust.F(true),
                    Input: braintrust.F(interaction0.input),
                    Output: braintrust.F(interaction1.output),
                    Metrics: braintrust.F(braintrust.ProjectLogInsertParamsEventsInsertProjectLogsEventMergeMetrics{
                        End: braintrust.F(getCurrentTime()),
                    }),
                },
            }),
        },
    )
}