Company Brief

Company Brief API

The app reads and trims sources in the browser and sends the model one request: the company, the numbered sources, and optionally your angle. The API takes the same input, so you can brief companies from your own pipeline (CRM notes, saved pages, filings) and check every line against the text you sent.

Base URL: https://api.skillsafe.ai/v1/app-api. Every response is {"data": ...} on success or {"error": {"code", "message"}} on failure. The request body is the input object - do not wrap it in {"input": ...}.

Input

FieldTypeNotes
companystring, requiredThe company the brief is about.
sourcesstring, requiredNumbered sources, each a header line [S1] Homepage · website (types: website, about, news, press, filing, notes, other) followed by its text, separated by a blank line. The app keeps this under about 48,000 characters.
anglestringOptional: why you are looking ("I sell X; first call with their CFO"). Adds the ANGLE section.
figuresstringOptional checklist of figures found in the sources, one per line as S3: $85 million - context.
clip_notestringOptional: what you trimmed from long sources.
retry_notestringOptional. The app sets it only on its one automatic reformat retry.

Output

The finished job's output.output is plain text: two header lines, then ## sections in a fixed order - WHAT_THEY_DO, CUSTOMERS, BUSINESS_MODEL, KEY_FIGURES, SIGNALS, STRENGTHS, WEAKNESSES, OPPORTUNITIES, RISKS, CONFLICTS, QUESTIONS, ANGLE (only with an angle) and GAPS.

COMPANY: Brightloom Labs
ONE_LINER: Brightloom Labs makes scheduling software for dental clinics. [S1] "builds scheduling software for dental clinics"
## CUSTOMERS
- The company says 1,200 practices use it. [S1] "Trusted by 1,200 practices"
## KEY_FIGURES
- Funding raised: $18 million (March 2025) [S2] "raised $18 million led by Northgate Ventures"
## RISKS
- (inferred) ... [S2] "..."
## GAPS
- No pricing or revenue information.

Each line ends with source tags and one or two quotes copied from a cited source. Search for each quote in that source before you trust the line - the app does exactly that and marks each line verified, close or not found. Lines starting (inferred) are judgement, not stated fact. An empty section reads - None found in the sources.

Errors

HTTPCodeMeaning
400VALIDATION_ERRORThe body is not a JSON object.
401UNAUTHORIZEDMissing or expired token.
402INSUFFICIENT_CREDITSBalance below the run's minimum. Top up at skillsafe.ai.
403FORBIDDENA guest token tried to run. Sign in for a personal token.
429RATE_LIMITEDToo many requests. Back off and retry.

1. A tiny client

# Every call: Authorization: Bearer $TOKEN, JSON in, {"data": ...} or {"error": ...} out.
TOKEN="YOUR_TOKEN"
BASE="https://api.skillsafe.ai/v1/app-api"
import json, urllib.request

BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN"

def call(method, path, body=None, headers=None):
    req = urllib.request.Request(BASE + path, method=method,
        data=None if body is None else json.dumps(body).encode(),
        headers={"Authorization": "Bearer " + TOKEN, "Content-Type": "application/json", **(headers or {})})
    with urllib.request.urlopen(req) as r:
        return json.load(r)["data"]
const BASE = "https://api.skillsafe.ai/v1/app-api";
const TOKEN = "YOUR_TOKEN";

async function call(method, path, body, headers = {}) {
  const res = await fetch(BASE + path, {
    method,
    headers: { Authorization: "Bearer " + TOKEN, "Content-Type": "application/json", ...headers },
    body: body === undefined ? undefined : JSON.stringify(body),
  });
  const json = await res.json();
  if (!res.ok) throw new Error(json.error ? json.error.code + ": " + json.error.message : res.status);
  return json.data;
}
package main

import (
	"bytes"
	"encoding/json"
	"net/http"
)

const base = "https://api.skillsafe.ai/v1/app-api"
const token = "YOUR_TOKEN"

func call(method, path string, body any, out any) error {
	var buf bytes.Buffer
	if body != nil {
		json.NewEncoder(&buf).Encode(body)
	}
	req, _ := http.NewRequest(method, base+path, &buf)
	req.Header.Set("Authorization", "Bearer "+token)
	req.Header.Set("Content-Type", "application/json")
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}
	defer res.Body.Close()
	var env struct{ Data json.RawMessage `json:"data"` }
	if err := json.NewDecoder(res.Body).Decode(&env); err != nil {
		return err
	}
	return json.Unmarshal(env.Data, out)
}
import java.net.URI;
import java.net.http.*;

public class Api {
  static final String BASE = "https://api.skillsafe.ai/v1/app-api";
  static final String TOKEN = "YOUR_TOKEN";
  static final HttpClient http = HttpClient.newHttpClient();

  static String call(String method, String path, String json) throws Exception {
    HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(BASE + path))
        .header("Authorization", "Bearer " + TOKEN)
        .header("Content-Type", "application/json");
    b = json == null ? b.method(method, HttpRequest.BodyPublishers.noBody())
                     : b.method(method, HttpRequest.BodyPublishers.ofString(json));
    return http.send(b.build(), HttpResponse.BodyHandlers.ofString()).body(); // {"data": ...}
  }
}
require "net/http"
require "json"

BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN"

def call(method, path, body = nil, headers = {})
  uri = URI(BASE + path)
  req = Net::HTTP.const_get(method.capitalize).new(uri)
  req["Authorization"] = "Bearer #{TOKEN}"
  req["Content-Type"] = "application/json"
  headers.each { |k, v| req[k] = v }
  req.body = body.to_json if body
  res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
  JSON.parse(res.body)["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
const TOKEN = "YOUR_TOKEN";

function call($method, $path, $body = null, $headers = []) {
  $ch = curl_init(BASE . $path);
  curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => $method,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => array_merge(["Authorization: Bearer " . TOKEN, "Content-Type: application/json"], $headers),
  ]);
  if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
  $out = json_decode(curl_exec($ch), true);
  return $out["data"] ?? $out;
}
using System.Net.Http;
using System.Text;
using System.Text.Json;

var http = new HttpClient { BaseAddress = new Uri("https://api.skillsafe.ai/v1/app-api/") };
http.DefaultRequestHeaders.Add("Authorization", "Bearer YOUR_TOKEN");

async Task<JsonElement> Call(HttpMethod m, string path, object? body = null) {
  var req = new HttpRequestMessage(m, path.TrimStart('/'));
  if (body != null) req.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
  var res = await http.SendAsync(req);
  using var doc = JsonDocument.Parse(await res.Content.ReadAsStringAsync());
  return doc.RootElement.GetProperty("data").Clone();
}

2. Get a token

Guests can price a run; running needs a personal token. The easiest way to get one is the token page.

# A guest token (enough for /me and /estimate). A run needs a personal token:
# sign in on https://company-brief.skillsafe.ai/tokens.html and copy it from there.
curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"company-brief"}'
guest = json.load(urllib.request.urlopen(urllib.request.Request(
    BASE + "/guest", data=json.dumps({"slug": "company-brief"}).encode(),
    headers={"Content-Type": "application/json"})))["data"]
print(guest["token"])  # guest: /me and /estimate only. Personal token: tokens.html
const g = await fetch(BASE + "/guest", {
  method: "POST", headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ slug: "company-brief" }),
}).then((r) => r.json());
console.log(g.data.token); // personal token for runs: tokens.html
// POST {"slug":"company-brief"} to base+"/guest" without a token; read data.token.
// For runs, copy a personal token from https://company-brief.skillsafe.ai/tokens.html
// POST {"slug":"company-brief"} to BASE + "/guest" (no Authorization header) and read data.token.
// Runs need a personal token from https://company-brief.skillsafe.ai/tokens.html
uri = URI(BASE + "/guest")
res = Net::HTTP.post(uri, { slug: "company-brief" }.to_json, "Content-Type" => "application/json")
puts JSON.parse(res.body)["data"]["token"]
$ch = curl_init(BASE . "/guest");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => ["Content-Type: application/json"], CURLOPT_POSTFIELDS => json_encode(["slug" => "company-brief"])]);
echo json_decode(curl_exec($ch), true)["data"]["token"];
var g = await new HttpClient().PostAsync("https://api.skillsafe.ai/v1/app-api/guest",
  new StringContent("{\"slug\":\"company-brief\"}", Encoding.UTF8, "application/json"));
Console.WriteLine(await g.Content.ReadAsStringAsync()); // data.token

3. Who am I, and my balance

curl -s "$BASE/me" -H "Authorization: Bearer $TOKEN"
me = call("GET", "/me")
print(me["subject_type"], me.get("credits"))
const me = await call("GET", "/me");
console.log(me.subject_type, me.credits);
var me map[string]any
call("GET", "/me", nil, &me)
System.out.println(Api.call("GET", "/me", null));
me = call("GET", "/me")
puts me["credits"]
$me = call("GET", "/me");
echo $me["credits"];
var me = await Call(HttpMethod.Get, "/me");
Console.WriteLine(me);

4. Price a brief (free)

hold_credits is the most a run can cost, reserved up front; you are charged only what it uses. 10,000 credits = $1.

curl -s -X POST "$BASE/estimate" -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" -d '{"company": "Brightloom Labs", "sources": "[S1] Homepage · website\nBrightloom Labs builds scheduling software for dental clinics. Trusted by 1,200 practices.\n\n[S2] Series A news · news\nMarch 2025 - Brightloom Labs raised $18 million led by Northgate Ventures. The company has 85 employees.", "angle": "I sell patient-messaging software; intro call with their COO."}'
body = {
    "company": "Brightloom Labs",
    "sources": "[S1] Homepage · website\nBrightloom Labs builds scheduling software for dental clinics. Trusted by 1,200 practices.\n\n[S2] Series A news · news\nMarch 2025 - Brightloom Labs raised $18 million led by Northgate Ventures. The company has 85 employees.",
    "angle": "I sell patient-messaging software; intro call with their COO."
}
est = call("POST", "/estimate", body)
print(est["hold_credits"], est["model_alias"], est.get("warnings"))
const body = {
  "company": "Brightloom Labs",
  "sources": "[S1] Homepage · website\nBrightloom Labs builds scheduling software for dental clinics. Trusted by 1,200 practices.\n\n[S2] Series A news · news\nMarch 2025 - Brightloom Labs raised $18 million led by Northgate Ventures. The company has 85 employees.",
  "angle": "I sell patient-messaging software; intro call with their COO."
};
const est = await call("POST", "/estimate", body);
console.log(est.hold_credits, est.model_alias, est.warnings);
body := map[string]string{
	"company": "Brightloom Labs",
	"sources": "[S1] Homepage · website\nBrightloom Labs builds scheduling software for dental clinics. Trusted by 1,200 practices.\n\n[S2] Series A news · news\nMarch 2025 - Brightloom Labs raised $18 million led by Northgate Ventures. The company has 85 employees.",
	"angle": "I sell patient-messaging software; intro call with their COO.",
}
var est map[string]any
call("POST", "/estimate", body, &est)
String body = "{\"company\": \"Brightloom Labs\", \"sources\": \"[S1] Homepage · website\\nBrightloom Labs builds scheduling software for dental clinics. Trusted by 1,200 practices.\\n\\n[S2] Series A news · news\\nMarch 2025 - Brightloom Labs raised $18 million led by Northgate Ventures. The company has 85 employees.\", \"angle\": \"I sell patient-messaging software; intro call with their COO.\"}";
System.out.println(Api.call("POST", "/estimate", body));
body = JSON.parse("{\"company\": \"Brightloom Labs\", \"sources\": \"[S1] Homepage · website\\nBrightloom Labs builds scheduling software for dental clinics. Trusted by 1,200 practices.\\n\\n[S2] Series A news · news\\nMarch 2025 - Brightloom Labs raised $18 million led by Northgate Ventures. The company has 85 employees.\", \"angle\": \"I sell patient-messaging software; intro call with their COO.\"}")
est = call("POST", "/estimate", body)
puts est["hold_credits"]
$body = json_decode("{\"company\": \"Brightloom Labs\", \"sources\": \"[S1] Homepage · website\\nBrightloom Labs builds scheduling software for dental clinics. Trusted by 1,200 practices.\\n\\n[S2] Series A news · news\\nMarch 2025 - Brightloom Labs raised $18 million led by Northgate Ventures. The company has 85 employees.\", \"angle\": \"I sell patient-messaging software; intro call with their COO.\"}", true);
$est = call("POST", "/estimate", $body);
echo $est["hold_credits"];
var body = JsonSerializer.Deserialize<JsonElement>("{\"company\": \"Brightloom Labs\", \"sources\": \"[S1] Homepage · website\\nBrightloom Labs builds scheduling software for dental clinics. Trusted by 1,200 practices.\\n\\n[S2] Series A news · news\\nMarch 2025 - Brightloom Labs raised $18 million led by Northgate Ventures. The company has 85 employees.\", \"angle\": \"I sell patient-messaging software; intro call with their COO.\"}");
var est = await Call(HttpMethod.Post, "/estimate", body);

5. Run it and poll

Always send an Idempotency-Key: a retry with the same key never bills twice.

curl -s -X POST "$BASE/run" -H "Authorization: Bearer $TOKEN" -H "Idempotency-Key: my-brief-001" \
  -H "Content-Type: application/json" -d '{"company": "Brightloom Labs", "sources": "[S1] Homepage · website\nBrightloom Labs builds scheduling software for dental clinics. Trusted by 1,200 practices.\n\n[S2] Series A news · news\nMarch 2025 - Brightloom Labs raised $18 million led by Northgate Ventures. The company has 85 employees.", "angle": "I sell patient-messaging software; intro call with their COO."}'
# -> data.job_id; then poll until status is succeeded or failed:
curl -s "$BASE/jobs/JOB_ID" -H "Authorization: Bearer $TOKEN"
import time
job = call("POST", "/run", body, {"Idempotency-Key": "my-brief-001"})
while job.get("status") not in ("succeeded", "failed"):
    time.sleep(1)
    job = call("GET", "/jobs/" + job["job_id"])
print(job["output"]["output"])  # COMPANY / ONE_LINER / ## sections
let job = await call("POST", "/run", body, { "Idempotency-Key": "my-brief-001" });
while (!["succeeded", "failed"].includes(job.status)) {
  await new Promise((r) => setTimeout(r, 1000));
  job = await call("GET", "/jobs/" + job.job_id);
}
console.log(job.output.output);
var job map[string]any
call("POST", "/run", body, &job) // set Idempotency-Key on the request too
for job["status"] != "succeeded" && job["status"] != "failed" {
	time.Sleep(time.Second)
	call("GET", "/jobs/"+job["job_id"].(string), nil, &job)
}
String job = Api.call("POST", "/run", body); // add header Idempotency-Key
// read data.job_id, then GET /jobs/{job_id} every second until status is succeeded or failed
job = call("POST", "/run", body, "Idempotency-Key" => "my-brief-001")
until %w[succeeded failed].include?(job["status"])
  sleep 1
  job = call("GET", "/jobs/#{job['job_id']}")
end
puts job["output"]["output"]
$job = call("POST", "/run", $body, ["Idempotency-Key: my-brief-001"]);
while (!in_array($job["status"], ["succeeded", "failed"])) {
  sleep(1);
  $job = call("GET", "/jobs/" . $job["job_id"]);
}
echo $job["output"]["output"];
var job = await Call(HttpMethod.Post, "/run", body); // add Idempotency-Key header
while (job.GetProperty("status").GetString() is not ("succeeded" or "failed")) {
  await Task.Delay(1000);
  job = await Call(HttpMethod.Get, "/jobs/" + job.GetProperty("job_id").GetString());
}

6. Or stream it

curl -N -X POST "$BASE/run-stream" -H "Authorization: Bearer $TOKEN" -H "Idempotency-Key: my-brief-002" \
  -H "Content-Type: application/json" -d '{"company": "Brightloom Labs", "sources": "[S1] Homepage · website\nBrightloom Labs builds scheduling software for dental clinics. Trusted by 1,200 practices.\n\n[S2] Series A news · news\nMarch 2025 - Brightloom Labs raised $18 million led by Northgate Ventures. The company has 85 employees.", "angle": "I sell patient-messaging software; intro call with their COO."}'
# event: job / event: delta {"text":...} / event: done {charged_credits, truncated, output}
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(body).encode(),
    headers={"Authorization": "Bearer " + TOKEN, "Content-Type": "application/json", "Idempotency-Key": "my-brief-002"})
with urllib.request.urlopen(req) as r:
    for line in r:
        print(line.decode().rstrip())  # event: / data: lines
const res = await fetch(BASE + "/run-stream", {
  method: "POST",
  headers: { Authorization: "Bearer " + TOKEN, "Content-Type": "application/json", "Idempotency-Key": "my-brief-002" },
  body: JSON.stringify(body),
});
const reader = res.body.getReader();
const dec = new TextDecoder();
for (;;) {
  const { done, value } = await reader.read();
  if (done) break;
  console.log(dec.decode(value)); // frames split on a blank line
}
// POST to base+"/run-stream" with the same headers plus Idempotency-Key,
// then read res.Body with bufio.Scanner: lines are "event: ..." and "data: {...}".
// POST to BASE + "/run-stream" and read HttpResponse.BodyHandlers.ofLines():
// "event: delta" lines carry data: {"text": ...}; "event: done" carries the final job.
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri, "Authorization" => "Bearer #{TOKEN}", "Content-Type" => "application/json", "Idempotency-Key" => "my-brief-002")
req.body = body.to_json
Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) { |res| res.read_body { |c| print c } } }
// Use CURLOPT_WRITEFUNCTION on a POST to BASE . "/run-stream" to receive SSE frames as they arrive.
var req = new HttpRequestMessage(HttpMethod.Post, "run-stream") {
  Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json") };
req.Headers.Add("Idempotency-Key", "my-brief-002");
var res = await http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
using var sr = new StreamReader(await res.Content.ReadAsStreamAsync());
while (await sr.ReadLineAsync() is string line) Console.WriteLine(line);