Ask the Gita

Ask the Gita API

The app finds the relevant verses in the browser and sends the model only the question and those verses. The API takes the same input, so you choose the verses (any edition, any search) and get back an answer whose every sentence about the text cites a verse you sent, with a word-for-word quotation per verse you can check.

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
questionstring, requiredThe question. The answer comes back in its language.
versesstring, requiredOne verse per line: [chapter.verse] Speaker: text, for example [2.47] The Blessed Lord: Thy business is with the action only, .... The app sends 5 to 12 verses; keep it under about 8,000 characters.
answer_lengthstringshort (3-6 sentences, default) or detailed (up to about 12).
conversationstringOptional earlier turns as User: / Assistant: lines, used only to resolve "it" or "that". Not evidence.
retry_notestringOptional. The app sets it only on its one automatic reformat retry.

Output

The finished job's output.output is plain text with five labelled sections:

STATUS: answered | partial | not_found
ANSWER:
Krishna tells Arjuna that his concern is the action itself and never its fruits [2.47]. He calls the stance of staying balanced evenly in success and failure yoga [2.48].
VERSES:
2.47: "Thy business is with the action only, never with its fruits"
2.48: "balanced evenly in success and failure: equilibrium is called yoga"
REFLECTION:
One way to live this is to give full care to today's task and meet the outcome calmly.
FOLLOW_UPS:
- What does the Gita say about acting without attachment?

Check each quotation against its verse before you trust it; the app does exactly that and marks each one verified, close or not found. A sentence about the text with no tag is uncited, and a tag naming a verse you did not send is flagged. REFLECTION is the model's gentle application, never the text itself.

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://ask-the-gita.skillsafe.ai/tokens.html and copy it from there.
curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"ask-the-gita"}'
guest = json.load(urllib.request.urlopen(urllib.request.Request(
    BASE + "/guest", data=json.dumps({"slug": "ask-the-gita"}).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: "ask-the-gita" }),
}).then((r) => r.json());
console.log(g.data.token); // personal token for runs: tokens.html
// POST {"slug":"ask-the-gita"} to base+"/guest" without a token; read data.token.
// For runs, copy a personal token from https://ask-the-gita.skillsafe.ai/tokens.html
// POST {"slug":"ask-the-gita"} to BASE + "/guest" (no Authorization header) and read data.token.
// Runs need a personal token from https://ask-the-gita.skillsafe.ai/tokens.html
uri = URI(BASE + "/guest")
res = Net::HTTP.post(uri, { slug: "ask-the-gita" }.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" => "ask-the-gita"])]);
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\":\"ask-the-gita\"}", 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 question (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 '{"question": "How do I stop worrying about whether my work will succeed?", "verses": "[2.47] The Blessed Lord: Thy business is with the action only, never with its fruits; so let not the fruit of action be thy motive, nor be thou to inaction attached.\n[2.48] The Blessed Lord: Perform action, O Dhananjaya, dwelling in union with the divine, renouncing attachments and balanced evenly in success and failure: equilibrium is called yoga.", "answer_length": "short"}'
body = {
    "question": "How do I stop worrying about whether my work will succeed?",
    "verses": "[2.47] The Blessed Lord: Thy business is with the action only, never with its fruits; so let not the fruit of action be thy motive, nor be thou to inaction attached.\n[2.48] The Blessed Lord: Perform action, O Dhananjaya, dwelling in union with the divine, renouncing attachments and balanced evenly in success and failure: equilibrium is called yoga.",
    "answer_length": "short"
}
est = call("POST", "/estimate", body)
print(est["hold_credits"], est["model_alias"], est.get("warnings"))
const body = {
  "question": "How do I stop worrying about whether my work will succeed?",
  "verses": "[2.47] The Blessed Lord: Thy business is with the action only, never with its fruits; so let not the fruit of action be thy motive, nor be thou to inaction attached.\n[2.48] The Blessed Lord: Perform action, O Dhananjaya, dwelling in union with the divine, renouncing attachments and balanced evenly in success and failure: equilibrium is called yoga.",
  "answer_length": "short"
};
const est = await call("POST", "/estimate", body);
console.log(est.hold_credits, est.model_alias, est.warnings);
body := map[string]string{
	"question": "How do I stop worrying about whether my work will succeed?",
	"verses": "[2.47] The Blessed Lord: Thy business is with the action only, never with its fruits; so let not the fruit of action be thy motive, nor be thou to inaction attached.\n[2.48] The Blessed Lord: Perform action, O Dhananjaya, dwelling in union with the divine, renouncing attachments and balanced evenly in success and failure: equilibrium is called yoga.",
	"answer_length": "short",
}
var est map[string]any
call("POST", "/estimate", body, &est)
String body = "{\"question\": \"How do I stop worrying about whether my work will succeed?\", \"verses\": \"[2.47] The Blessed Lord: Thy business is with the action only, never with its fruits; so let not the fruit of action be thy motive, nor be thou to inaction attached.\\n[2.48] The Blessed Lord: Perform action, O Dhananjaya, dwelling in union with the divine, renouncing attachments and balanced evenly in success and failure: equilibrium is called yoga.\", \"answer_length\": \"short\"}";
System.out.println(Api.call("POST", "/estimate", body));
body = JSON.parse("{\"question\": \"How do I stop worrying about whether my work will succeed?\", \"verses\": \"[2.47] The Blessed Lord: Thy business is with the action only, never with its fruits; so let not the fruit of action be thy motive, nor be thou to inaction attached.\\n[2.48] The Blessed Lord: Perform action, O Dhananjaya, dwelling in union with the divine, renouncing attachments and balanced evenly in success and failure: equilibrium is called yoga.\", \"answer_length\": \"short\"}")
est = call("POST", "/estimate", body)
puts est["hold_credits"]
$body = json_decode("{\"question\": \"How do I stop worrying about whether my work will succeed?\", \"verses\": \"[2.47] The Blessed Lord: Thy business is with the action only, never with its fruits; so let not the fruit of action be thy motive, nor be thou to inaction attached.\\n[2.48] The Blessed Lord: Perform action, O Dhananjaya, dwelling in union with the divine, renouncing attachments and balanced evenly in success and failure: equilibrium is called yoga.\", \"answer_length\": \"short\"}", true);
$est = call("POST", "/estimate", $body);
echo $est["hold_credits"];
var body = JsonSerializer.Deserialize<JsonElement>("{\"question\": \"How do I stop worrying about whether my work will succeed?\", \"verses\": \"[2.47] The Blessed Lord: Thy business is with the action only, never with its fruits; so let not the fruit of action be thy motive, nor be thou to inaction attached.\\n[2.48] The Blessed Lord: Perform action, O Dhananjaya, dwelling in union with the divine, renouncing attachments and balanced evenly in success and failure: equilibrium is called yoga.\", \"answer_length\": \"short\"}");
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-q-001" \
  -H "Content-Type: application/json" -d '{"question": "How do I stop worrying about whether my work will succeed?", "verses": "[2.47] The Blessed Lord: Thy business is with the action only, never with its fruits; so let not the fruit of action be thy motive, nor be thou to inaction attached.\n[2.48] The Blessed Lord: Perform action, O Dhananjaya, dwelling in union with the divine, renouncing attachments and balanced evenly in success and failure: equilibrium is called yoga.", "answer_length": "short"}'
# -> 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-q-001"})
while job.get("status") not in ("succeeded", "failed"):
    time.sleep(1)
    job = call("GET", "/jobs/" + job["job_id"])
print(job["output"]["output"])  # STATUS / ANSWER / VERSES / REFLECTION / FOLLOW_UPS
let job = await call("POST", "/run", body, { "Idempotency-Key": "my-q-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-q-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-q-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-q-002" \
  -H "Content-Type: application/json" -d '{"question": "How do I stop worrying about whether my work will succeed?", "verses": "[2.47] The Blessed Lord: Thy business is with the action only, never with its fruits; so let not the fruit of action be thy motive, nor be thou to inaction attached.\n[2.48] The Blessed Lord: Perform action, O Dhananjaya, dwelling in union with the divine, renouncing attachments and balanced evenly in success and failure: equilibrium is called yoga.", "answer_length": "short"}'
# 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-q-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-q-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-q-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-q-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);