Drive Statement to Sheet from your own code
Everything the web app's AI reader does goes through one public surface. (Text PDFs are parsed in the browser and never touch the API; this page is about the paid AI read.) Base URL:
https://api.skillsafe.ai/v1/app-api
Every request carries Authorization: Bearer <token> and Content-Type: application/json.
Every response is a JSON envelope: {"ok":true,"data":{…}} on success and
{"ok":false,"error":{"code":"…","message":"…"}} on failure. Read error.code, not only the HTTP status.
Errors
| Code | Meaning | What to do |
|---|---|---|
UNAUTHORIZED | Missing, expired or wrong-app token. | Mint a guest token or sign in again. |
INSUFFICIENT_CREDITS | Balance below min_credits. | Top up. /estimate is free and tells you the hold. |
VALIDATION_ERROR | The input object failed validation. | Check error.details. The body is the input object itself — never wrap it in {"input": …}. |
NOT_FOUND | A $files id does not exist for this token's subject. | Upload again with the same token you run with. |
RATE_LIMITED | Too many requests. | Back off and retry. |
JOB_FAILED | The run started and did not complete. | Retry with the same Idempotency-Key. |
The input
One task, "extract", with two sources. Fields are scalars; the object-valued ones (hints, facts) travel as JSON strings.
| Field | Type | Meaning |
|---|---|---|
task | string, required | Always "extract". |
source | string | "images" (page scans or photos attached as $files) or "text" (a PDF's text layer in text). |
$files | array of file ids | Images uploaded first with POST /files (step 3). Up to 8 per run. The web app shrinks each to 1600 px on the long side as JPEG and deletes it after the run. |
text | string | The text layer, lines in reading order, cells separated by two spaces, --- page N --- before each page. The web app sends at most 24,000 characters, cut on whole lines with a [... N lines cut ...] marker. |
file_names | string | File names, comma-separated, in $files order. |
hints | string (JSON) | Optional currency, date_order (MDY/DMY), year. |
facts | string (JSON) | What the free parser already measured (why AI is needed, totals found, rows read). |
retry_note | string | Sent only on the one automatic reformat retry. |
The output contract
The output (output.output) is one JSON object: {"task":"extract","documents":[…],"notes":""}. Each document has
source, doc_type (bank_statement · credit_card · receipt · bill · other), account (institution, name, last4, currency),
period (start, end, ISO dates), totals (opening_balance, closing_balance, total_in, total_out as printed, or null),
transactions[] (date, description, amount, balance, category, page, uncertain), receipt (merchant, date, items, subtotal, tax, tip, fees, discount, total, payment) or null, unreadable[] and notes.
Sign convention: amount is positive for money in and negative for money out, from the account holder's side — a card purchase is negative, a card payment positive. Amounts are JSON numbers with no separators. The model transcribes totals as printed and never adjusts a number to make them agree; the web app then reconciles opening + in − out against closing, the rows against the printed totals, and the running balance row by row. If you call the API directly, run that reconciliation yourself.
1. Get a token
A guest token is enough for /me and /estimate. Running is metered and needs a personal token from signing in. The tokens page shows yours and copies it. Replace YOUR_TOKEN below with it.
2. Check the session and the balance
/me returns subject_type, subject_id and credits. Signed in means subject_type === "user".
curl -s "https://api.skillsafe.ai/v1/app-api/me" \ -H "Authorization: Bearer YOUR_TOKEN"
import requests
TOKEN = "YOUR_TOKEN"
BASE = "https://api.skillsafe.ai/v1/app-api"
H = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"}
r = requests.get(f"{BASE}/me", headers=H, timeout=30).json()
print(r["data"]["subject_type"], r["data"]["credits"])
const TOKEN = "YOUR_TOKEN";
const BASE = "https://api.skillsafe.ai/v1/app-api";
const H = { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" };
const r = await (await fetch(`${BASE}/me`, { headers: H })).json();
console.log(r.data.subject_type, r.data.credits);
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
const token = "YOUR_TOKEN"
func main() {
req, _ := http.NewRequest("GET", base+"/me", nil)
req.Header.Set("Authorization", "Bearer "+token)
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
var out map[string]any
json.NewDecoder(res.Body).Decode(&out)
fmt.Println(out["data"])
_ = bytes.NewReader
}
var token = "YOUR_TOKEN";
var base = "https://api.skillsafe.ai/v1/app-api";
var http = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder(java.net.URI.create(base + "/me"))
.header("Authorization", "Bearer " + token).GET().build();
System.out.println(http.send(req, java.net.http.HttpResponse.BodyHandlers.ofString()).body());
require "net/http"
require "json"
TOKEN = "YOUR_TOKEN"
BASE = "https://api.skillsafe.ai/v1/app-api"
uri = URI("#{BASE}/me")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
puts JSON.parse(res.body)["data"]
<?php
$token = "YOUR_TOKEN";
$base = "https://api.skillsafe.ai/v1/app-api";
$ch = curl_init("$base/me");
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ["Authorization: Bearer $token"]]);
print_r(json_decode(curl_exec($ch), true)["data"]);
using System.Net.Http.Headers;
using System.Text;
var token = "YOUR_TOKEN";
var baseUrl = "https://api.skillsafe.ai/v1/app-api";
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
Console.WriteLine(await http.GetStringAsync($"{baseUrl}/me"));
3. Upload the page images
POST /files as multipart/form-data with a file part; data.file.file_id is what goes in $files. Delete it afterwards with DELETE /files/FILE_ID.
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/files" \ -H "Authorization: Bearer YOUR_TOKEN" \ -F "file=@receipt.jpg;type=image/jpeg"
4. Price it before you run it
/estimate is free and starts no job. hold_credits is a reservation priced against the longest possible answer, not the price. Estimate with the real $files attached: each image adds to the hold. Images:
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/estimate" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"task": "extract", "source": "images", "file_names": "receipt.jpg", "hints": "{\"currency\": \"USD\"}", "facts": "{\"why\": \"photo - no text layer\", \"files\": 1}", "$files": ["FILE_ID_FROM_UPLOAD"]}'
import requests
TOKEN = "YOUR_TOKEN"
BASE = "https://api.skillsafe.ai/v1/app-api"
H = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"}
body = {
"task": "extract",
"source": "images",
"file_names": "receipt.jpg",
"hints": "{\"currency\": \"USD\"}",
"facts": "{\"why\": \"photo - no text layer\", \"files\": 1}",
"$files": [
"FILE_ID_FROM_UPLOAD"
]
}
r = requests.post(f"{BASE}/estimate", headers={**H}, json=body, timeout=120).json()
print(r["data"]["hold_credits"], r["data"]["model_alias"])
const TOKEN = "YOUR_TOKEN";
const BASE = "https://api.skillsafe.ai/v1/app-api";
const H = { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" };
const body = {
"task": "extract",
"source": "images",
"file_names": "receipt.jpg",
"hints": "{\"currency\": \"USD\"}",
"facts": "{\"why\": \"photo - no text layer\", \"files\": 1}",
"$files": [
"FILE_ID_FROM_UPLOAD"
]
};
const r = await (await fetch(`${BASE}/estimate`, { method: "POST", headers: { ...H }, body: JSON.stringify(body) })).json();
console.log(r.data.hold_credits, r.data.model_alias);
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
const token = "YOUR_TOKEN"
func main() {
payload := []byte(`{"task": "extract", "source": "images", "file_names": "receipt.jpg", "hints": "{\"currency\": \"USD\"}", "facts": "{\"why\": \"photo - no text layer\", \"files\": 1}", "$files": ["FILE_ID_FROM_UPLOAD"]}`)
req, _ := http.NewRequest("POST", base+"/estimate", bytes.NewReader(payload))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
var out map[string]any
json.NewDecoder(res.Body).Decode(&out)
fmt.Println(out["data"])
}
var token = "YOUR_TOKEN";
var base = "https://api.skillsafe.ai/v1/app-api";
var http = java.net.http.HttpClient.newHttpClient();
var body = "{\"task\": \"extract\", \"source\": \"images\", \"file_names\": \"receipt.jpg\", \"hints\": \"{\\\"currency\\\": \\\"USD\\\"}\", \"facts\": \"{\\\"why\\\": \\\"photo - no text layer\\\", \\\"files\\\": 1}\", \"$files\": [\"FILE_ID_FROM_UPLOAD\"]}";
var req = java.net.http.HttpRequest.newBuilder(java.net.URI.create(base + "/estimate"))
.header("Authorization", "Bearer " + token).header("Content-Type", "application/json")
.POST(java.net.http.HttpRequest.BodyPublishers.ofString(body)).build();
System.out.println(http.send(req, java.net.http.HttpResponse.BodyHandlers.ofString()).body());
require "net/http"
require "json"
TOKEN = "YOUR_TOKEN"
BASE = "https://api.skillsafe.ai/v1/app-api"
uri = URI("#{BASE}/estimate")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req.body = "{\"task\": \"extract\", \"source\": \"images\", \"file_names\": \"receipt.jpg\", \"hints\": \"{\\\"currency\\\": \\\"USD\\\"}\", \"facts\": \"{\\\"why\\\": \\\"photo - no text layer\\\", \\\"files\\\": 1}\", \"$files\": [\"FILE_ID_FROM_UPLOAD\"]}"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
puts JSON.parse(res.body)["data"]
<?php
$token = "YOUR_TOKEN";
$base = "https://api.skillsafe.ai/v1/app-api";
$body = "{\"task\": \"extract\", \"source\": \"images\", \"file_names\": \"receipt.jpg\", \"hints\": \"{\\\"currency\\\": \\\"USD\\\"}\", \"facts\": \"{\\\"why\\\": \\\"photo - no text layer\\\", \\\"files\\\": 1}\", \"$files\": [\"FILE_ID_FROM_UPLOAD\"]}";
$ch = curl_init("$base/estimate");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => $body,
CURLOPT_HTTPHEADER => ["Authorization: Bearer $token", "Content-Type: application/json"],
]);
print_r(json_decode(curl_exec($ch), true)["data"]);
using System.Net.Http.Headers;
using System.Text;
var token = "YOUR_TOKEN";
var baseUrl = "https://api.skillsafe.ai/v1/app-api";
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var body = new StringContent("{\"task\": \"extract\", \"source\": \"images\", \"file_names\": \"receipt.jpg\", \"hints\": \"{\\\"currency\\\": \\\"USD\\\"}\", \"facts\": \"{\\\"why\\\": \\\"photo - no text layer\\\", \\\"files\\\": 1}\", \"$files\": [\"FILE_ID_FROM_UPLOAD\"]}", Encoding.UTF8, "application/json");
var res = await http.PostAsync($"{baseUrl}/estimate", body);
Console.WriteLine(await res.Content.ReadAsStringAsync());
Text (a PDF whose text layer you already have):
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/estimate" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"task": "extract", "source": "text", "file_names": "statement.pdf", "text": "--- page 1 ---\nStatement period 09/01/2026 - 09/30/2026\nBeginning balance $2,418.37\nEnding balance $2,322.41\nDate Description Withdrawals Deposits Balance\n09/03 Card purchase Greenleaf Grocery 84.62 2,333.75\n09/30 Interest paid 0.38 2,334.13\n09/30 Monthly service fee 12.00 2,322.13", "hints": "{\"currency\": \"USD\", \"date_order\": \"MDY\"}", "facts": "{\"why\": \"the free parser'\''s rows did not reconcile\", \"free_parser_rows\": 3}"}'
import requests
TOKEN = "YOUR_TOKEN"
BASE = "https://api.skillsafe.ai/v1/app-api"
H = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"}
body = {
"task": "extract",
"source": "text",
"file_names": "statement.pdf",
"text": "--- page 1 ---\nStatement period 09/01/2026 - 09/30/2026\nBeginning balance $2,418.37\nEnding balance $2,322.41\nDate Description Withdrawals Deposits Balance\n09/03 Card purchase Greenleaf Grocery 84.62 2,333.75\n09/30 Interest paid 0.38 2,334.13\n09/30 Monthly service fee 12.00 2,322.13",
"hints": "{\"currency\": \"USD\", \"date_order\": \"MDY\"}",
"facts": "{\"why\": \"the free parser's rows did not reconcile\", \"free_parser_rows\": 3}"
}
r = requests.post(f"{BASE}/estimate", headers={**H}, json=body, timeout=120).json()
print(r["data"]["hold_credits"])
const TOKEN = "YOUR_TOKEN";
const BASE = "https://api.skillsafe.ai/v1/app-api";
const H = { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" };
const body = {
"task": "extract",
"source": "text",
"file_names": "statement.pdf",
"text": "--- page 1 ---\nStatement period 09/01/2026 - 09/30/2026\nBeginning balance $2,418.37\nEnding balance $2,322.41\nDate Description Withdrawals Deposits Balance\n09/03 Card purchase Greenleaf Grocery 84.62 2,333.75\n09/30 Interest paid 0.38 2,334.13\n09/30 Monthly service fee 12.00 2,322.13",
"hints": "{\"currency\": \"USD\", \"date_order\": \"MDY\"}",
"facts": "{\"why\": \"the free parser's rows did not reconcile\", \"free_parser_rows\": 3}"
};
const r = await (await fetch(`${BASE}/estimate`, { method: "POST", headers: { ...H }, body: JSON.stringify(body) })).json();
console.log(r.data.hold_credits);
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
const token = "YOUR_TOKEN"
func main() {
payload := []byte(`{"task": "extract", "source": "text", "file_names": "statement.pdf", "text": "--- page 1 ---\nStatement period 09/01/2026 - 09/30/2026\nBeginning balance $2,418.37\nEnding balance $2,322.41\nDate Description Withdrawals Deposits Balance\n09/03 Card purchase Greenleaf Grocery 84.62 2,333.75\n09/30 Interest paid 0.38 2,334.13\n09/30 Monthly service fee 12.00 2,322.13", "hints": "{\"currency\": \"USD\", \"date_order\": \"MDY\"}", "facts": "{\"why\": \"the free parser's rows did not reconcile\", \"free_parser_rows\": 3}"}`)
req, _ := http.NewRequest("POST", base+"/estimate", bytes.NewReader(payload))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
var out map[string]any
json.NewDecoder(res.Body).Decode(&out)
fmt.Println(out["data"])
}
var token = "YOUR_TOKEN";
var base = "https://api.skillsafe.ai/v1/app-api";
var http = java.net.http.HttpClient.newHttpClient();
var body = "{\"task\": \"extract\", \"source\": \"text\", \"file_names\": \"statement.pdf\", \"text\": \"--- page 1 ---\\nStatement period 09/01/2026 - 09/30/2026\\nBeginning balance $2,418.37\\nEnding balance $2,322.41\\nDate Description Withdrawals Deposits Balance\\n09/03 Card purchase Greenleaf Grocery 84.62 2,333.75\\n09/30 Interest paid 0.38 2,334.13\\n09/30 Monthly service fee 12.00 2,322.13\", \"hints\": \"{\\\"currency\\\": \\\"USD\\\", \\\"date_order\\\": \\\"MDY\\\"}\", \"facts\": \"{\\\"why\\\": \\\"the free parser's rows did not reconcile\\\", \\\"free_parser_rows\\\": 3}\"}";
var req = java.net.http.HttpRequest.newBuilder(java.net.URI.create(base + "/estimate"))
.header("Authorization", "Bearer " + token).header("Content-Type", "application/json")
.POST(java.net.http.HttpRequest.BodyPublishers.ofString(body)).build();
System.out.println(http.send(req, java.net.http.HttpResponse.BodyHandlers.ofString()).body());
require "net/http"
require "json"
TOKEN = "YOUR_TOKEN"
BASE = "https://api.skillsafe.ai/v1/app-api"
uri = URI("#{BASE}/estimate")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req.body = "{\"task\": \"extract\", \"source\": \"text\", \"file_names\": \"statement.pdf\", \"text\": \"--- page 1 ---\\nStatement period 09/01/2026 - 09/30/2026\\nBeginning balance $2,418.37\\nEnding balance $2,322.41\\nDate Description Withdrawals Deposits Balance\\n09/03 Card purchase Greenleaf Grocery 84.62 2,333.75\\n09/30 Interest paid 0.38 2,334.13\\n09/30 Monthly service fee 12.00 2,322.13\", \"hints\": \"{\\\"currency\\\": \\\"USD\\\", \\\"date_order\\\": \\\"MDY\\\"}\", \"facts\": \"{\\\"why\\\": \\\"the free parser's rows did not reconcile\\\", \\\"free_parser_rows\\\": 3}\"}"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
puts JSON.parse(res.body)["data"]
<?php
$token = "YOUR_TOKEN";
$base = "https://api.skillsafe.ai/v1/app-api";
$body = "{\"task\": \"extract\", \"source\": \"text\", \"file_names\": \"statement.pdf\", \"text\": \"--- page 1 ---\\nStatement period 09/01/2026 - 09/30/2026\\nBeginning balance $2,418.37\\nEnding balance $2,322.41\\nDate Description Withdrawals Deposits Balance\\n09/03 Card purchase Greenleaf Grocery 84.62 2,333.75\\n09/30 Interest paid 0.38 2,334.13\\n09/30 Monthly service fee 12.00 2,322.13\", \"hints\": \"{\\\"currency\\\": \\\"USD\\\", \\\"date_order\\\": \\\"MDY\\\"}\", \"facts\": \"{\\\"why\\\": \\\"the free parser's rows did not reconcile\\\", \\\"free_parser_rows\\\": 3}\"}";
$ch = curl_init("$base/estimate");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => $body,
CURLOPT_HTTPHEADER => ["Authorization: Bearer $token", "Content-Type: application/json"],
]);
print_r(json_decode(curl_exec($ch), true)["data"]);
using System.Net.Http.Headers;
using System.Text;
var token = "YOUR_TOKEN";
var baseUrl = "https://api.skillsafe.ai/v1/app-api";
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var body = new StringContent("{\"task\": \"extract\", \"source\": \"text\", \"file_names\": \"statement.pdf\", \"text\": \"--- page 1 ---\\nStatement period 09/01/2026 - 09/30/2026\\nBeginning balance $2,418.37\\nEnding balance $2,322.41\\nDate Description Withdrawals Deposits Balance\\n09/03 Card purchase Greenleaf Grocery 84.62 2,333.75\\n09/30 Interest paid 0.38 2,334.13\\n09/30 Monthly service fee 12.00 2,322.13\", \"hints\": \"{\\\"currency\\\": \\\"USD\\\", \\\"date_order\\\": \\\"MDY\\\"}\", \"facts\": \"{\\\"why\\\": \\\"the free parser's rows did not reconcile\\\", \\\"free_parser_rows\\\": 3}\"}", Encoding.UTF8, "application/json");
var res = await http.PostAsync($"{baseUrl}/estimate", body);
Console.WriteLine(await res.Content.ReadAsStringAsync());
5. Run and poll
/run returns a job. Always send an Idempotency-Key — the same key is the same run, so a retry cannot bill twice. The web app derives it from a hash of the input.
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/run" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: statement-to-sheet:extract:8f3ka2-1x:a1" \
-d '{"task": "extract", "source": "images", "file_names": "receipt.jpg", "hints": "{\"currency\": \"USD\"}", "facts": "{\"why\": \"photo - no text layer\", \"files\": 1}", "$files": ["FILE_ID_FROM_UPLOAD"]}'
import requests
TOKEN = "YOUR_TOKEN"
BASE = "https://api.skillsafe.ai/v1/app-api"
H = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"}
body = {
"task": "extract",
"source": "images",
"file_names": "receipt.jpg",
"hints": "{\"currency\": \"USD\"}",
"facts": "{\"why\": \"photo - no text layer\", \"files\": 1}",
"$files": [
"FILE_ID_FROM_UPLOAD"
]
}
r = requests.post(f"{BASE}/run", headers={**H, "Idempotency-Key": "statement-to-sheet:extract:8f3ka2-1x:a1"}, json=body, timeout=120).json()
job_id = r["data"]["job_id"]
const TOKEN = "YOUR_TOKEN";
const BASE = "https://api.skillsafe.ai/v1/app-api";
const H = { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" };
const body = {
"task": "extract",
"source": "images",
"file_names": "receipt.jpg",
"hints": "{\"currency\": \"USD\"}",
"facts": "{\"why\": \"photo - no text layer\", \"files\": 1}",
"$files": [
"FILE_ID_FROM_UPLOAD"
]
};
const r = await (await fetch(`${BASE}/run`, { method: "POST", headers: { ...H, "Idempotency-Key": "statement-to-sheet:extract:8f3ka2-1x:a1" }, body: JSON.stringify(body) })).json();
const jobId = r.data.job_id;
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
const token = "YOUR_TOKEN"
func main() {
payload := []byte(`{"task": "extract", "source": "images", "file_names": "receipt.jpg", "hints": "{\"currency\": \"USD\"}", "facts": "{\"why\": \"photo - no text layer\", \"files\": 1}", "$files": ["FILE_ID_FROM_UPLOAD"]}`)
req, _ := http.NewRequest("POST", base+"/run", bytes.NewReader(payload))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "statement-to-sheet:extract:8f3ka2-1x:a1")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
var out map[string]any
json.NewDecoder(res.Body).Decode(&out)
fmt.Println(out["data"])
}
var token = "YOUR_TOKEN";
var base = "https://api.skillsafe.ai/v1/app-api";
var http = java.net.http.HttpClient.newHttpClient();
var body = "{\"task\": \"extract\", \"source\": \"images\", \"file_names\": \"receipt.jpg\", \"hints\": \"{\\\"currency\\\": \\\"USD\\\"}\", \"facts\": \"{\\\"why\\\": \\\"photo - no text layer\\\", \\\"files\\\": 1}\", \"$files\": [\"FILE_ID_FROM_UPLOAD\"]}";
var req = java.net.http.HttpRequest.newBuilder(java.net.URI.create(base + "/run"))
.header("Authorization", "Bearer " + token).header("Content-Type", "application/json")
.header("Idempotency-Key", "statement-to-sheet:extract:8f3ka2-1x:a1")
.POST(java.net.http.HttpRequest.BodyPublishers.ofString(body)).build();
System.out.println(http.send(req, java.net.http.HttpResponse.BodyHandlers.ofString()).body());
require "net/http"
require "json"
TOKEN = "YOUR_TOKEN"
BASE = "https://api.skillsafe.ai/v1/app-api"
uri = URI("#{BASE}/run")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req["Idempotency-Key"] = "statement-to-sheet:extract:8f3ka2-1x:a1"
req.body = "{\"task\": \"extract\", \"source\": \"images\", \"file_names\": \"receipt.jpg\", \"hints\": \"{\\\"currency\\\": \\\"USD\\\"}\", \"facts\": \"{\\\"why\\\": \\\"photo - no text layer\\\", \\\"files\\\": 1}\", \"$files\": [\"FILE_ID_FROM_UPLOAD\"]}"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
puts JSON.parse(res.body)["data"]
<?php
$token = "YOUR_TOKEN";
$base = "https://api.skillsafe.ai/v1/app-api";
$body = "{\"task\": \"extract\", \"source\": \"images\", \"file_names\": \"receipt.jpg\", \"hints\": \"{\\\"currency\\\": \\\"USD\\\"}\", \"facts\": \"{\\\"why\\\": \\\"photo - no text layer\\\", \\\"files\\\": 1}\", \"$files\": [\"FILE_ID_FROM_UPLOAD\"]}";
$ch = curl_init("$base/run");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => $body,
CURLOPT_HTTPHEADER => ["Authorization: Bearer $token", "Content-Type: application/json", "Idempotency-Key: statement-to-sheet:extract:8f3ka2-1x:a1"],
]);
print_r(json_decode(curl_exec($ch), true)["data"]);
using System.Net.Http.Headers;
using System.Text;
var token = "YOUR_TOKEN";
var baseUrl = "https://api.skillsafe.ai/v1/app-api";
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var body = new StringContent("{\"task\": \"extract\", \"source\": \"images\", \"file_names\": \"receipt.jpg\", \"hints\": \"{\\\"currency\\\": \\\"USD\\\"}\", \"facts\": \"{\\\"why\\\": \\\"photo - no text layer\\\", \\\"files\\\": 1}\", \"$files\": [\"FILE_ID_FROM_UPLOAD\"]}", Encoding.UTF8, "application/json");
http.DefaultRequestHeaders.Add("Idempotency-Key", "statement-to-sheet:extract:8f3ka2-1x:a1");
var res = await http.PostAsync($"{baseUrl}/run", body);
Console.WriteLine(await res.Content.ReadAsStringAsync());
curl -s "https://api.skillsafe.ai/v1/app-api/jobs/JOB_ID" \ -H "Authorization: Bearer YOUR_TOKEN" # repeat until data.status is "succeeded" or "failed"; # the result is the JSON string at data.output.output
import requests
TOKEN = "YOUR_TOKEN"
BASE = "https://api.skillsafe.ai/v1/app-api"
H = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"}
import json, time
job_id = "JOB_ID"
while True:
job = requests.get(f"{BASE}/jobs/{job_id}", headers=H, timeout=30).json()["data"]
if job["status"] in ("succeeded", "failed"):
break
time.sleep(1)
result = json.loads(job["output"]["output"])
for d in result["documents"]:
print(d["doc_type"], len(d["transactions"]), d["totals"])
const TOKEN = "YOUR_TOKEN";
const BASE = "https://api.skillsafe.ai/v1/app-api";
const H = { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" };
let job;
for (;;) {
job = (await (await fetch(`${BASE}/jobs/JOB_ID`, { headers: H })).json()).data;
if (job.status === "succeeded" || job.status === "failed") break;
await new Promise((r) => setTimeout(r, 1000));
}
const result = JSON.parse(job.output.output);
for (const d of result.documents) console.log(d.doc_type, d.transactions.length, d.totals);
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
const token = "YOUR_TOKEN"
// GET base+"/jobs/JOB_ID" every second until data.status is
// "succeeded" or "failed", then json.Unmarshal data.output.output.
func main() { fmt.Println("poll", base+"/jobs/JOB_ID"); _ = bytes.NewReader; _ = json.Marshal; _ = http.Get }
var token = "YOUR_TOKEN"; var base = "https://api.skillsafe.ai/v1/app-api"; var http = java.net.http.HttpClient.newHttpClient(); // GET base + "/jobs/JOB_ID" every second until data.status is // "succeeded" or "failed"; the result is the JSON string at data.output.output.
require "net/http"
require "json"
TOKEN = "YOUR_TOKEN"
BASE = "https://api.skillsafe.ai/v1/app-api"
loop do
uri = URI("#{BASE}/jobs/JOB_ID")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
job = JSON.parse(Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }.body)["data"]
if %w[succeeded failed].include?(job["status"])
puts JSON.parse(job["output"]["output"])["documents"]
break
end
sleep 1
end
<?php
$token = "YOUR_TOKEN";
$base = "https://api.skillsafe.ai/v1/app-api";
do {
$ch = curl_init("$base/jobs/JOB_ID");
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ["Authorization: Bearer $token"]]);
$job = json_decode(curl_exec($ch), true)["data"];
if (!in_array($job["status"], ["succeeded", "failed"])) sleep(1);
} while (!in_array($job["status"], ["succeeded", "failed"]));
print_r(json_decode($job["output"]["output"], true)["documents"]);
using System.Net.Http.Headers;
using System.Text;
var token = "YOUR_TOKEN";
var baseUrl = "https://api.skillsafe.ai/v1/app-api";
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
// GET {baseUrl}/jobs/JOB_ID every second until data.status is
// "succeeded" or "failed"; the result is the JSON string at data.output.output.
6. Stream it
/run-stream answers with server-sent events and ends with a done frame carrying the whole output, charged_credits and truncated. If truncated is true the balance ran low mid-answer: parse the complete rows that arrived and top up for the rest.
curl -sN -X POST "https://api.skillsafe.ai/v1/app-api/run-stream" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-H "Idempotency-Key: statement-to-sheet:extract:8f3ka2-1x:a1" \
-d '{"task": "extract", "source": "images", "file_names": "receipt.jpg", "hints": "{\"currency\": \"USD\"}", "facts": "{\"why\": \"photo - no text layer\", \"files\": 1}", "$files": ["FILE_ID_FROM_UPLOAD"]}'
# event: job data: {"job_id": ...}
# event: delta data: {"text": "..."} (may be absent: a browser often gets only
# event: tick heartbeats and the whole answer in the final frame)
# event: done data: {"output": {"output": "<the JSON result>"}, "charged_credits": ..., "truncated": false}
import requests
TOKEN = "YOUR_TOKEN"
BASE = "https://api.skillsafe.ai/v1/app-api"
H = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"}
import json
body = {"task": "extract", "source": "images", "file_names": "receipt.jpg", "hints": "{\"currency\": \"USD\"}", "facts": "{\"why\": \"photo - no text layer\", \"files\": 1}", "$files": ["FILE_ID_FROM_UPLOAD"]}
with requests.post(f"{BASE}/run-stream", json=body, stream=True, timeout=300,
headers={**H, "Accept": "text/event-stream", "Idempotency-Key": "statement-to-sheet:extract:8f3ka2-1x:a1"}) as r:
event, done = None, None
for line in r.iter_lines(decode_unicode=True):
if line.startswith("event:"): event = line[6:].strip()
elif line.startswith("data:") and event == "done": done = json.loads(line[5:])
result = json.loads(done["output"]["output"])
print(len(result["documents"]), done.get("charged_credits"), done.get("truncated"))
const TOKEN = "YOUR_TOKEN";
const BASE = "https://api.skillsafe.ai/v1/app-api";
const H = { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" };
const res = await fetch(`${BASE}/run-stream`, { method: "POST",
headers: { ...H, Accept: "text/event-stream", "Idempotency-Key": "statement-to-sheet:extract:8f3ka2-1x:a1" },
body: JSON.stringify({"task": "extract", "source": "images", "file_names": "receipt.jpg", "hints": "{\"currency\": \"USD\"}", "facts": "{\"why\": \"photo - no text layer\", \"files\": 1}", "$files": ["FILE_ID_FROM_UPLOAD"]}) });
const text = await res.text(); // or read res.body incrementally
let done = null;
for (const frame of text.split("\n\n")) {
const ev = /^event: *(.*)$/m.exec(frame), data = /^data: *(.*)$/m.exec(frame);
if (ev && ev[1] === "done" && data) done = JSON.parse(data[1]);
}
const result = JSON.parse(done.output.output);
console.log(result.documents.length, done.truncated);
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
const token = "YOUR_TOKEN"
// POST base+"/run-stream" with Accept: text/event-stream, read lines with
// bufio.Scanner, and json.Unmarshal the data line of the "done" event.
func main() { fmt.Println("see the cURL tab for the frame format"); _ = bytes.NewReader; _ = json.Marshal; _ = http.Get }
var token = "YOUR_TOKEN"; var base = "https://api.skillsafe.ai/v1/app-api"; var http = java.net.http.HttpClient.newHttpClient(); // POST base + "/run-stream" with Accept: text/event-stream, read the body // line by line (BodyHandlers.ofLines()), and parse the data line of the "done" event.
require "net/http"
require "json"
TOKEN = "YOUR_TOKEN"
BASE = "https://api.skillsafe.ai/v1/app-api"
# POST to "#{BASE}/run-stream" with Accept: text/event-stream; read the body,
# split frames on blank lines, and JSON.parse the data line of the "done" event.
<?php $token = "YOUR_TOKEN"; $base = "https://api.skillsafe.ai/v1/app-api"; // POST to "$base/run-stream" with Accept: text/event-stream; split the body on // blank lines and json_decode the data line of the "done" event.
using System.Net.Http.Headers;
using System.Text;
var token = "YOUR_TOKEN";
var baseUrl = "https://api.skillsafe.ai/v1/app-api";
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
// POST {baseUrl}/run-stream with Accept: text/event-stream; read the response
// stream line by line and parse the data line of the "done" event.