Drive Card Data Desk from your own code
The page does its free work in the browser: it turns six answers into the PCI DSS v4.0.1 SAQ each channel
points at and the merchant level, scans the pasted code, config, page markup, schema and logs with 23 rules
(PC-01..PC-23: card numbers, track data, card codes, logs, hashes, TLS, card fields, payment-page scripts,
secrets), and masks card numbers, track data, card-code values and secrets. Its two paid lanes are available
over HTTP. You send one JSON object with task set to "scope" or
"review", and the model replies with one JSON object.
Base URL: https://api.skillsafe.ai/v1/app-api. Every call sends
Content-Type: application/json and, once you have a token,
Authorization: Bearer <token>. Both lanes run on gpt-terra with a 10%
publisher markup. A run needs a signed-in (user) token and is metered: the platform holds
the estimate's hold_credits while the run is in flight and then charges only
what it used (charged_credits). /estimate is free. A plain-text summary is at
/llms.txt.
The response envelope
Every response is {"data": ..., "error": null} on success or {"data": null, "error": {"code", "message"}} on failure.
Error codes
| HTTP | code | meaning |
|---|---|---|
| 400 | VALIDATION_ERROR | The body is not a JSON object, or a field is the wrong type. Every field of the run input is a string. |
| 401 | UNAUTHORIZED | No token, or an expired one: get a new one (step 2). |
| 402 | INSUFFICIENT_CREDITS | The balance is below the run's minimum. Top up at skillsafe.ai/account/billing/. |
| 403 | FORBIDDEN | A guest token tried a metered run: sign in for a personal token. |
| 404 | NOT_FOUND | Unknown job id. |
| 429 | RATE_LIMITED | Back off and retry with the same Idempotency-Key. |
The task field: two lanes
| task | what the model does |
|---|---|
"scope" | Reads the SAQ, the level, the systems in scope and why, how card data flows, the requirements that carry the weight and how to shrink the scope; answers every scope flag. Runs from the answers or a description alone; code may be empty. |
"review" | Reviews the masked paste: rates storage, transmission, software, access and logging, gives findings with exact before/after edits, answers every flag. Needs code. |
Input fields (all strings)
| field | content | |
|---|---|---|
task | required | scope or review |
facts | required | A JSON string: answers, saq (the page's SAQ reading: saq, channels, level, validation, conflicts, saq_if_paste, notes), files (path, kind, paste lines), components, flags (uid, id, sev, req, area, file, line, text, detail), other_flags (scope lane), masked, notes. It is PCI.facts(A, answers, lane) in pcikit.js. |
code | optional for scope | The paste as the page sends it: file-marker lines between files, card numbers cut to the first six and last four, track data, card-code values and secrets replaced. Mask your own paste the same way before sending it. |
description | optional | How payments work, in your words (at most 8,000 characters). |
note | optional | A goal or constraint (at most 2,000 characters). |
scope_handoff | review only | A JSON string of a scope reply's saq, level, headline, components and data_flows. |
retry_note | optional | Sent only when a previous reply could not be read. |
The output contracts
task: "scope"
headline, saq (A, A-EP, B, B-IP, C-VT, C, P2PE, SPoC, D-Merchant, D-SP, multiple, undetermined), saq_agrees, saq_reason, level (1-4, SP1, SP2, unknown), level_note, channels[{channel, saq, why}], components[{name, category: cde|connected|security|out_of_scope|third_party, why, evidence}], data_flows[{step, data: PAN|PAN+SAD|token|masked|none, where}], requirements[{req, applies: yes|no|check, why, evidence}], reductions[], flag_answers[{flag, status: confirmed|dismissed, reason}], questions[], summary.
task: "review"
verdict (blocked, needs_work, ready), headline, system, areas[{area, status: sound|fragile|broken, note}], findings[{id, flags, req, file, line, severity, title, why, evidence, edits[{before, after}]}], flag_answers, plan[], questions[], summary.
Worked examples
These are the page's own bundled examples (invented businesses), with the input as app.js builds it and the saved model reply the page replays for free.
Scope: a candle shop on an iframe online and a virtual terminal by phone
Input (facts and code shortened here):
{
"task": "scope",
"facts": "(a JSON string; decoded, its saq is multiple, level 3, with 0 flags)",
"code": "<!-- file: site/checkout.html -->\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <title>Checkout - Brackwillow Candle Co.</title>\n <script src=\"https://js.stripe.com/v3/\"></script>\n <script src=\"https://widget.chatbubble.example.net/loader.js\" async></script>\n <script src=\"https://cdn.metricsly.example.net/tag.js\"></script>\n <script src=\"/js/checkout.js\" defer></script>\n</head>\n<body data-stripe-key=\"publishable-key-from-template\">\n <h1>Checkout</h1>\n <p id=\"order-summary\"></p>\n <form id=\"payment-form\">\n <div id=\"payment-element\"></div>\n <button id=\"submit\" typ... (2549 characters in all)",
"description": "Brackwillow Candle Co. sells hand-poured candles online and by phone. The website is our own Node/Express app on a rented VPS; the checkout page uses Stripe's Payment Element, and we never see card numbers online. The checkout page also loads a live-chat widget and our analytics tag.\n\nPhone orders: staff write the order on a paper slip, then type the card number into the virtual terminal in the Stripe Dashboard on the office laptop. The same laptop is used for email and the shop's bookkeeping. Slips are kept in a drawer and shredded at the end of each month.\n\nWe do not keep card numbers in any system. About 31,000 Visa online orders and 7,000 Visa phone orders a year.",
"note": "Our bank sent the annual PCI letter. Which SAQ do we fill in, and is there anything we should change first?"
}
Reply:
{
"task": "scope",
"headline": "SAQ is multiple - SAQ A for the e-commerce iframe, but the phone channel doesn't actually qualify for C-VT yet - and you're Level 3 on 31,000 Visa e-commerce transactions a year.",
"saq": "multiple",
"saq_agrees": false,
"saq_reason": "Ecommerce checks out as facts computed it: the checkout page only mounts Stripe's Payment Element into an iframe (site/checkout.html:16, site/js/checkout.js:29), so SAQ A holds. But facts.saq's own condition for the phone channel's SAQ C-VT is that the virtual-terminal computer is 'used only for that, is isolated from your other systems' - and the description says 'The same laptop is used for email and the shop's bookkeeping.' That's new information the answers didn't carry, and it directly fails the condition. As described today, the phone channel doesn't meet SAQ C-VT's eligibility and should be scoped as SAQ D-Merchant until that laptop is dedicated to the virtual terminal alone. Your own notes already flag that SAQ D-Merchant is the one SAQ that would cover the whole mix - that's a real option here, not just a fallback.",
"level": "3",
"level_note": "31,000 Visa e-commerce transactions a year falls in Visa's 20,000-1,000,000 range for Level 3; your acquirer assigns the final level, and the 7,000 phone orders don't change the e-commerce count that set it.",
"channels": [
{
"channel": "ecommerce",
... (shortened here; the page shows it whole)
Review: an outdoor shop whose own server takes the card
Input:
{
"task": "review",
"facts": "(a JSON string; decoded, its saq is D-Merchant, level 3, with 23 flags)",
"code": "<!-- file: checkout/templates/pay.html -->\n<form action=\"/pay\" method=\"post\">\n <input type=\"hidden\" name=\"amount\" value=\"{{ amount }}\">\n <input type=\"text\" name=\"card_number\" autocomplete=\"cc-number\" placeholder=\"Card number\">\n <input type=\"text\" name=\"exp\" autocomplete=\"cc-exp\" placeholder=\"MM/YY\">\n <input type=\"text\" name=\"cvv\" autocomplete=\"cc-csc\" placeholder=\"CVV\">\n <button type=\"submit\">Pay</button>\n</form>\n# file: checkout/app.py\nimport hashlib\nimport logging\n\nimport requests\nfrom flask import Flask, render_template, request\n\nfrom .db import Payment, db\n\napp = Flask(__name__)\nlog =... (2436 characters in all)",
"description": "Skerrabrae Outfitters' web shop: our own Flask checkout takes the card on our page, posts it to our server, and the server charges it through Quayrelay's API. We keep the payment record for refunds and chargebacks.",
"note": "We are moving to a processor-hosted checkout next quarter. What has to change right now?"
}
Reply:
{
"task": "review",
"verdict": "blocked",
"headline": "Blocked: your Flask server builds the raw card number and CVV into the Quayrelay request itself (app.py:31), and the database and the log already keep the PAN and CVV in the clear.",
"system": "checkout/templates/pay.html posts a card_number/exp/cvv form straight to your own Flask /pay handler in checkout/app.py, which forwards the raw values to Quayrelay's charge API over http:// with certificate checks off, logs the card number and CVV twice, and writes both into a Payment row in checkout/db.py; logs/checkout.log shows three real transactions where this already happened.",
"areas": [
{
"area": "storage",
"status": "broken",
"note": "app.py:36 and db.py:47-48 keep the CVV and the full card number in the Payment row; app.py:35's fingerprint is an unkeyed SHA-1 of the PAN; logs/checkout.log:66-69 already hold the live card number and CVV for three transactions."
},
{
"area": "transmission",
"status": "broken",
"note": "GATEWAY_URL is http:// (app.py:20), the Quayrelay call disables certificate verification (app.py:33), and nginx.conf:55 still allows TLSv1 and TLSv1.1."
},
{
"area": "software",
"status": "broken",
"note": "pay.html:4-6 carries its own card_number/exp/cvv fields and app.py:31 builds them into the Quayrelay request itself, so your server - not a hosted field - handles t
... (shortened here; the page shows it whole)
Review: a yoga studio on Stripe Checkout that is ready
Input:
{
"task": "review",
"facts": "(a JSON string; decoded, its saq is A, level 4, with 0 flags)",
"code": "<!-- file: app/templates/pricing.html -->\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'self'; script-src 'self'; form-action 'self' https://checkout.stripe.com\">\n <title>Class passes - Vellmarsh Yoga Studio</title>\n <link rel=\"stylesheet\" href=\"/static/site.css\">\n</head>\n<body>\n <h1>Class passes</h1>\n <form action=\"/buy/ten-class\" method=\"post\">\n <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\">\n <button type=\"submit\">Ten-class pass - go to checkout</button>\n </form>\n <form actio... (2332 characters in all)",
"description": "Vellmarsh Yoga Studio sells class passes on its website. The \"go to checkout\" buttons create a Stripe Checkout session and redirect to Stripe's own payment page; a webhook marks the pass paid. The front desk takes cash only. About 4,000 Visa card payments a year, all online.",
"note": "Quick check before we renew our SAQ A."
}
Reply:
{
"task": "review",
"verdict": "ready",
"headline": "Ready: Stripe Checkout keeps every card field off Vellmarsh's servers and the webhook signature is verified - no PCI findings to fix.",
"system": "The Flask app redirects both class-pass purchases to a Stripe Checkout Session (app/payments.py:35-41); Stripe hosts the entire payment page and the merchant's server never receives a PAN. A signature-verified webhook (lines 44-53) reads only the Stripe session id to mark the pass paid.",
"areas": [
{
"area": "storage",
"status": "sound",
"note": "Nothing in this paste stores card data: payments.py only ever holds a Stripe session id and a plan/price/quantity, and there is no database or model code in the paste at all."
},
{
"area": "transmission",
"status": "sound",
"note": "nginx.conf allows only TLSv1.2/TLSv1.3 (line 66) with HSTS set (line 67). The proxy_pass to 127.0.0.1:8000 (line 69) is plain http, but that hop never carries card data under this SAQ A redirect flow - Stripe holds the PAN end to end, and the app only ever exchanges a plan name, a Stripe session id and a webhook event."
},
{
"area": "software",
"status": "sound",
"note": "pricing.html's two forms (lines 12-19) post only a CSRF token to /buy/<plan>; there is no card field on the page. payments.py creates the Checkout Session server-side and redirects the browser to Stri
... (shortened here; the page shows it whole)
The page re-checks the reply; the API does not
The page holds every reply to account (recon.js): the SAQ against its reading of the answers, the level against the counts, quotes against the description and paste, every finding's file, line and evidence, every edit applied in order and the scan run again, every flag answered once, and no unmasked card number. Over the API you get the model's reply as it is; run the same checks yourself, or paste the reply's input into the page.
Step 1 — a tiny client helper
One function that sends JSON and unwraps the envelope.
BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here" # from https://pci-desk.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://pci-desk.skillsafe.ai/tokens.html
def api(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={"Content-Type": "application/json", **({"Authorization": "Bearer " + TOKEN} if TOKEN else {}), **(headers or {})})
with urllib.request.urlopen(req) as r:
env = json.load(r)
if env.get("error"):
raise RuntimeError(env["error"])
return env["data"]
// pci-desk.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://pci-desk.skillsafe.ai/tokens.html
async function api(method, path, body, headers = {}) {
const r = await fetch(BASE + path, { method, headers: { "Content-Type": "application/json",
...(TOKEN ? { Authorization: "Bearer " + TOKEN } : {}), ...headers },
body: body === undefined ? undefined : JSON.stringify(body) });
const env = await r.json();
if (env.error) throw new Error(env.error.code + ": " + env.error.message);
return env.data;
}
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
var token = "paste-your-token-here" // from https://pci-desk.skillsafe.ai/tokens.html
func api(method, path string, body any, headers map[string]string) (map[string]any, error) {
var buf bytes.Buffer
if body != nil { json.NewEncoder(&buf).Encode(body) }
req, _ := http.NewRequest(method, base+path, &buf)
req.Header.Set("Content-Type", "application/json")
if token != "" { req.Header.Set("Authorization", "Bearer "+token) }
for k, v := range headers { req.Header.Set(k, v) }
res, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer res.Body.Close()
var env struct { Data map[string]any; Error map[string]any }
json.NewDecoder(res.Body).Decode(&env)
if env.Error != nil { return nil, fmt.Errorf("%v", env.Error) }
return env.Data, nil
}
// PciDesk.java -- Java 17+, with com.fasterxml.jackson.core:jackson-databind
import com.fasterxml.jackson.databind.*;
import java.net.URI; import java.net.http.*; import java.util.*;
public class PciDesk {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://pci-desk.skillsafe.ai/tokens.html
static final ObjectMapper M = new ObjectMapper();
static final HttpClient C = HttpClient.newHttpClient();
static JsonNode api(String method, String path, Object body, Map<String, String> headers) throws Exception {
var b = HttpRequest.newBuilder(URI.create(BASE + path)).header("Content-Type", "application/json");
if (!token.isEmpty()) b.header("Authorization", "Bearer " + token);
if (headers != null) headers.forEach(b::header);
b.method(method, body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(M.writeValueAsString(body)));
JsonNode env = M.readTree(C.send(b.build(), HttpResponse.BodyHandlers.ofString()).body());
if (env.hasNonNull("error")) throw new RuntimeException(env.get("error").toString());
return env.get("data");
}
}
require "json"
require "net/http"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://pci-desk.skillsafe.ai/tokens.html
def api(method, path, body = nil, headers = {})
uri = URI(BASE + path)
req = Net::HTTPGenericRequest.new(method, !body.nil?, true, uri.request_uri,
{ "Content-Type" => "application/json" }.merge(TOKEN.empty? ? {} : { "Authorization" => "Bearer #{TOKEN}" }).merge(headers))
req.body = JSON.generate(body) unless body.nil?
env = JSON.parse(Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }.body)
raise env["error"].to_s if env["error"]
env["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = "paste-your-token-here"; // from https://pci-desk.skillsafe.ai/tokens.html
function api(string $method, string $path, $body = null, array $headers = []) {
global $TOKEN;
$h = ["Content-Type: application/json"];
if ($TOKEN) $h[] = "Authorization: Bearer $TOKEN";
foreach ($headers as $k => $v) $h[] = "$k: $v";
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $h, CURLOPT_RETURNTRANSFER => true]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$env = json_decode(curl_exec($ch), true);
if (!empty($env["error"])) throw new Exception(json_encode($env["error"]));
return $env["data"];
}
// Program.cs -- .NET 6+ console app
using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes;
static class Api {
public const string Base = "https://api.skillsafe.ai/v1/app-api";
public static string Token = "paste-your-token-here"; // from https://pci-desk.skillsafe.ai/tokens.html
static readonly HttpClient C = new();
public static async Task<JsonNode?> Call(string method, string path, object? body = null, Dictionary<string, string>? headers = null) {
var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
if (Token != "") req.Headers.Add("Authorization", "Bearer " + Token);
if (headers != null) foreach (var h in headers) req.Headers.Add(h.Key, h.Value);
if (body != null) req.Content = JsonContent.Create(body);
var env = JsonNode.Parse(await (await C.SendAsync(req)).Content.ReadAsStringAsync())!;
if (env["error"] is JsonNode e && e.GetValueKind() != JsonValueKind.Null) throw new Exception(e.ToJsonString());
return env["data"];
}
}
Step 2 — a token
A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"pci-desk"}' | jq -r '.data.token')
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = ""
TOKEN = api("POST", "/guest", {"slug": "pci-desk"})["token"]
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = "";
TOKEN = (await api("POST", "/guest", { slug: "pci-desk" })).token;
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = ""
g, err := api("POST", "/guest", map[string]string{"slug": "pci-desk"}, nil)
if err != nil { panic(err) }
token = g["token"].(string)
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = "";
token = api("POST", "/guest", Map.of("slug", "pci-desk"), null).get("token").asText();
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN.replace("")
TOKEN.replace(api("POST", "/guest", { slug: "pci-desk" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "pci-desk"])["token"];
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
Api.Token = "";
Api.Token = (await Api.Call("POST", "/guest", new { slug = "pci-desk" }))!["token"]!.GetValue<string>();
Step 3 — GET /me
Who the token is (subject_type: user or guest) and the credit balance.
api GET /me | jq '.data | {subject_type, credits}'
me = api("GET", "/me")
print(me["subject_type"], me.get("credits"))
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
me, _ := api("GET", "/me", nil, nil)
fmt.Println(me["subject_type"], me["credits"])
JsonNode me = api("GET", "/me", null, null);
System.out.println(me.get("subject_type") + " " + me.get("credits"));
me = api("GET", "/me")
puts [me["subject_type"], me["credits"]].inspect
$me = api("GET", "/me");
echo $me["subject_type"], " ", $me["credits"] ?? "", "\n";
var me = await Api.Call("GET", "/me");
Console.WriteLine($"{me!["subject_type"]} {me["credits"]}");
Step 4 — POST /estimate
Free. The body is the run input itself (not wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model reads gpt-5.6-terra and model_alias gpt-terra.
# input.json: the run input object - {"task": "scope", "facts": "...", "code": "...", "description": "..."}
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}'
inp = json.load(open("input.json")) # {"task": "scope", "facts": "...", "code": "...", ...}
est = api("POST", "/estimate", inp)
print(est["hold_credits"], est["min_credits"], est["model"], est.get("warnings"))
import { readFileSync } from "node:fs";
const input = JSON.parse(readFileSync("input.json", "utf8")); // {task, facts, code, description, ...}
const est = await api("POST", "/estimate", input);
console.log(est.hold_credits, est.min_credits, est.model, est.warnings);
raw, _ := os.ReadFile("input.json") // add "os" to the imports
var input map[string]any
json.Unmarshal(raw, &input)
est, _ := api("POST", "/estimate", input, nil)
fmt.Println(est["hold_credits"], est["min_credits"], est["model"])
Map<String, Object> input = M.readValue(new java.io.File("input.json"), Map.class);
JsonNode est = api("POST", "/estimate", input, null);
System.out.println(est.get("hold_credits") + " " + est.get("min_credits") + " " + est.get("model"));
input = JSON.parse(File.read("input.json"))
est = api("POST", "/estimate", input)
puts [est["hold_credits"], est["min_credits"], est["model"]].inspect
$input = json_decode(file_get_contents("input.json"), true);
$est = api("POST", "/estimate", $input);
echo $est["hold_credits"], " ", $est["min_credits"], " ", $est["model"], "\n";
var input = JsonNode.Parse(File.ReadAllText("input.json"))!;
var est = await Api.Call("POST", "/estimate", input);
Console.WriteLine($"{est!["hold_credits"]} {est["min_credits"]} {est["model"]}");
Step 5 — POST /run, then poll the job
Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses pci-desk:<lane>:<inputHash>:a<attempt>. The job's output.output is the reply text; truncated: true means the balance cut it short.
KEY="pci-desk:scope:$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json # one JSON object, the lane's contract
echo "$J" | jq '.data | {charged_credits, truncated}'
import hashlib, time
key = "pci-desk:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", inp, {"Idempotency-Key": key})
while True:
j = api("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
reply = json.loads(j["output"]["output"]) # the lane's JSON object
print(j["charged_credits"], j.get("truncated"), reply.get("saq") or reply.get("verdict"))
import { createHash } from "node:crypto";
const key = `pci-desk:${input.task}:${createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16)}:a1`;
const job = await api("POST", "/run", input, { "Idempotency-Key": key });
let j;
do { await new Promise((r) => setTimeout(r, 2000)); j = await api("GET", "/jobs/" + job.job_id); }
while (!["succeeded", "failed", "cancelled"].includes(j.status));
const reply = JSON.parse(j.output.output);
console.log(j.charged_credits, j.truncated, reply.saq ?? reply.verdict);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "pci-desk:" + input["task"].(string) + ":" + hex.EncodeToString(sum[:8]) + ":a1"
job, _ := api("POST", "/run", input, map[string]string{"Idempotency-Key": key})
var j map[string]any
for {
time.Sleep(2 * time.Second)
j, _ = api("GET", "/jobs/"+job["job_id"].(string), nil, nil)
if s := j["status"]; s == "succeeded" || s == "failed" || s == "cancelled" { break }
}
fmt.Println(j["charged_credits"], j["truncated"], j["output"].(map[string]any)["output"])
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "pci-desk:" + input.get("task") + ":" + HexFormat.of().formatHex(digest, 0, 8) + ":a1";
JsonNode job = api("POST", "/run", input, Map.of("Idempotency-Key", key)), j;
do { Thread.sleep(2000); j = api("GET", "/jobs/" + job.get("job_id").asText(), null, null); }
while (!Set.of("succeeded", "failed", "cancelled").contains(j.get("status").asText()));
JsonNode reply = M.readTree(j.get("output").get("output").asText());
System.out.println(j.get("charged_credits") + " " + reply);
require "digest"
key = "pci-desk:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["saq"] || reply["verdict"]].inspect
$key = "pci-desk:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", $reply["saq"] ?? $reply["verdict"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"pci-desk:{input["task"]}:{Convert.ToHexString(digest, 0, 8).ToLowerInvariant()}:a1";
var job = await Api.Call("POST", "/run", input, new() { ["Idempotency-Key"] = key });
JsonNode j;
do { await Task.Delay(2000); j = (await Api.Call("GET", "/jobs/" + job!["job_id"]))!; }
while (!new[] { "succeeded", "failed", "cancelled" }.Contains(j["status"]!.GetValue<string>()));
var reply = JsonNode.Parse(j["output"]!["output"]!.GetValue<string>());
Console.WriteLine($"{j["charged_credits"]} {reply}");
Step 6 — POST /run-stream (review)
The same run as Server-Sent Events. review.json is a review input, with scope_handoff when it follows a scope reading.
# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: pci-desk:review:$(shasum -a 256 review.json | cut -c1-16):a1" --data @review.json
rev = json.load(open("review.json")) # {"task": "review", "facts": "...", "code": "...", "scope_handoff": "..."}
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "pci-desk:review:" + hashlib.sha256(json.dumps(rev).encode()).hexdigest()[:16] + ":a1"})
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().strip()
if line.startswith("data:"):
print(line[5:].strip()[:200])
const rev = JSON.parse(readFileSync("review.json", "utf8"));
const r = await fetch(BASE + "/run-stream", { method: "POST", body: JSON.stringify(rev), headers: {
"Content-Type": "application/json", Authorization: "Bearer " + TOKEN,
"Idempotency-Key": `pci-desk:review:${createHash("sha256").update(JSON.stringify(rev)).digest("hex").slice(0, 16)}:a1` } });
const dec = new TextDecoder();
for await (const chunk of r.body) for (const line of dec.decode(chunk).split("\n"))
if (line.startsWith("data:")) console.log(line.slice(5).trim().slice(0, 200));
rraw, _ := os.ReadFile("review.json") // add "bufio", "strings" to the imports
rs := sha256.Sum256(rraw)
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(rraw))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", "pci-desk:review:"+hex.EncodeToString(rs[:8])+":a1")
res, _ := http.DefaultClient.Do(req)
sc := bufio.NewScanner(res.Body)
for sc.Scan() { if l := sc.Text(); strings.HasPrefix(l, "data:") { fmt.Println(l[5:]) } }
String rev = java.nio.file.Files.readString(java.nio.file.Path.of("review.json"));
var rd = java.security.MessageDigest.getInstance("SHA-256").digest(rev.getBytes());
var sreq = HttpRequest.newBuilder(URI.create(BASE + "/run-stream")).header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token).header("Idempotency-Key", "pci-desk:review:" + HexFormat.of().formatHex(rd, 0, 8) + ":a1")
.POST(HttpRequest.BodyPublishers.ofString(rev)).build();
C.send(sreq, HttpResponse.BodyHandlers.ofLines()).body().filter(l -> l.startsWith("data:")).forEach(System.out::println);
rev = File.read("review.json")
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}",
"Idempotency-Key" => "pci-desk:review:#{Digest::SHA256.hexdigest(rev)[0, 16]}:a1")
req.body = rev
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |h|
h.request(req) { |res| res.read_body { |chunk| chunk.each_line { |l| puts l if l.start_with?("data:") } } }
end
$rev = file_get_contents("review.json");
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $rev, CURLOPT_HTTPHEADER => [
"Content-Type: application/json", "Authorization: Bearer $TOKEN",
"Idempotency-Key: pci-desk:review:" . substr(hash("sha256", $rev), 0, 16) . ":a1"],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) { foreach (explode("\n", $chunk) as $l) if (str_starts_with($l, "data:")) echo $l, "\n"; return strlen($chunk); }]);
curl_exec($ch);
var rev = File.ReadAllText("review.json");
var rd = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(rev));
var sreq = new HttpRequestMessage(HttpMethod.Post, Api.Base + "/run-stream") { Content = new StringContent(rev, System.Text.Encoding.UTF8, "application/json") };
sreq.Headers.Add("Authorization", "Bearer " + Api.Token);
sreq.Headers.Add("Idempotency-Key", $"pci-desk:review:{Convert.ToHexString(rd, 0, 8).ToLowerInvariant()}:a1");
using var http = new HttpClient();
using var res = await http.SendAsync(sreq, HttpCompletionOption.ResponseHeadersRead);
using var rdr = new StreamReader(await res.Content.ReadAsStreamAsync());
for (string? l; (l = await rdr.ReadLineAsync()) != null;) if (l.StartsWith("data:")) Console.WriteLine(l);