← Documentation
ENEnglish · Lietuviškai

Blackoutly API — integration with other systems

Blackoutly Enterprise. A guide for the integrator or administrator connecting Blackoutly to another system of the organisation: document management, case management, archive, e-delivery or your own scripts. Below, the external system is any such system.

1. How the API works

The Blackoutly API is a command line with a JSON result, not a network service. There is no HTTP / REST API — on purpose. The program never connects to the network for documents and listens on no port (the principle: documents never leave the computer). Integration happens locally, through:

  1. the command line BlackoutlyCli.exe — the external system (its service, a scheduled task, a script) starts it as a process;
  2. files in folders — input documents, review packages (*.bo-review.json), results (*_bo_*.pdf);
  3. JSON output (--json) and exit codes — the external system learns what succeeded.

A person always confirms (the program's principle). So what is automated is not everything, but what is around the person:

The program API: external system → prepare → employee A → employee B → apply → external system

prepare saves time (the suggestions are ready in advance), apply saves only what a second person approved. Without an external system the same is done in the program's window (B may also save it themselves).

2. Requirements

3. Commands

BlackoutlyCli.exe prepare <files or folders>… [--review-dir FOLDER] [--json] [--no-ocr] [--force]
BlackoutlyCli.exe apply   <files or folders>… [--review-dir FOLDER] [-o RESULT_FOLDER] [--json] [--no-ocr]
Parameter Meaning
<files or folders> PDF, JPG, TIFF files; a folder — all such files in it (not its subfolders, not *_bo_* results)
--review-dir The review package folder (default: policy ReviewFolder, otherwise next to the file)
-o, --output-dir Where to save the results (default: policy DefaultOutputDir, otherwise next to the original)
--json The result as a JSON array on standard output (without it — lines for a person)
--no-ocr Do not recognise scans (faster; scanned pages get no suggestions)
--force prepare: overwrite a package a person is already reviewing or has approved (normally do not use)

prepare — prepare for review

Opens the files, finds the suggestions (for all the files together — a surname found in one is looked for in all) and creates a review package with status draft for each. An employee who opens that file in the program sees the suggestions; once submitted — submitted. prepare does not touch a file a person is already reviewing (submitted / approved).

At most 50 files at a time (the Enterprise limit; the others are skipped with a warning — run again), each up to 500 pages.

apply — save the approved ones

For each file, looks for a package with the same SHA-256 and status approved, prepared and approved by different people. Then: rotates the pages as in the review, blacks out the approved places (with the legal grounds), creates a new PDF, checks the result (if the check finds problems the file is not saved, status blocked), writes an audit record (interface: cli, prepared_by, approved_by) and removes the package. The original is never changed.

apply is not "save anyway" — check failures cannot be bypassed.

4. Result

Exit codes

Code prepare apply
0 all files prepared or skipped all files saved
2 at least one file with an error at least one not saved (not approved, stopped by the check, an error) or no files found
3 not an Enterprise license not an Enterprise license
1 an unexpected program error (the text — on standard error)

Code 2 is not a failure: the JSON shows which files to process later.

JSON (--json)

An array, one object per file. The field detail is an explanation for a person (in Lithuanian) — do not parse it, use status.

prepare:

[
  {"file": "D:\\Integration\\inbox\\claim.pdf", "status": "prepared",
   "package": "\\\\server\\blackoutly\\review\\4f2a9c0e1b7d3a55.bo-review.json", "suggestions": 14},
  {"file": "D:\\Integration\\inbox\\contract.pdf", "status": "skipped",
   "detail": "jau peržiūrimas ar patvirtintas (--force — perrašyti)"},
  {"file": "D:\\Integration\\inbox\\damaged.pdf", "status": "error", "detail": "…"}
]
status Meaning
prepared The package was created (package), with suggestions suggestions
skipped A person is already reviewing or has approved the file — nothing done
error The file could not be opened (password, damaged, too many pages)

apply:

[
  {"file": "D:\\Integration\\inbox\\claim.pdf", "status": "saved",
   "output": "D:\\Integration\\outbox\\claim_bo_20261018_142301.pdf", "redactions": 16,
   "output_sha256": "9b1c…"},
  {"file": "D:\\Integration\\inbox\\contract.pdf", "status": "not_approved", "detail": "būsena: submitted"},
  {"file": "D:\\Integration\\inbox\\complaint.pdf", "status": "blocked", "detail": "Paslėpta reikšmė vis dar matoma 2 puslapyje."}
]
status Meaning What the external system does
saved The result output, checked Take the result, mark the case as anonymised
not_approved No approved package (still under review), or the same person prepared and approved it Try again later
blocked The check found a hidden value still there — the file was not saved Return it to the employee to fix
error The file could not be opened Tell the administrator

output_sha256 matches the audit log record — the external system's record can be linked to the audit without keeping file names.

5. Examples

5.1. PowerShell: a scheduled task every 10 minutes

# C:\Scripts\blackoutly-api.ps1 — run by Task Scheduler as the service account
$Cli    = "C:\Program Files\Blackoutly\BlackoutlyCli.exe"
$Inbox  = "\\server\blackoutly\inbox"      # the external system puts documents to anonymise here
$Review = "\\server\blackoutly\review"     # review packages (the same as policy ReviewFolder)
$Outbox = "\\server\blackoutly\outbox"     # results for the external system
$Done   = "\\server\blackoutly\done"       # processed originals

[Console]::OutputEncoding = [Text.UTF8Encoding]::new($false)

# 1) new files -> suggestions for the employees
$prepared = & $Cli prepare $Inbox --review-dir $Review --json | ConvertFrom-Json
if ($LASTEXITCODE -eq 3) { throw "A Blackoutly Enterprise license is needed" }
$prepared | Where-Object status -eq "error" | ForEach-Object { Write-Warning "$($_.file): $($_.detail)" }

# 2) approved files -> results
$applied = & $Cli apply $Inbox --review-dir $Review -o $Outbox --json | ConvertFrom-Json
foreach ($r in $applied) {
    switch ($r.status) {
        "saved"   { Move-Item $r.file $Done -Force; Write-Host "Anonymised: $($r.output)" }
        "blocked" { Write-Warning "Stopped by the check: $($r.file) — return it to the employee" }
        default   { }   # not_approved: still under review, next time
    }
}

The task: Task Scheduler → Create Task → run powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\Scripts\blackoutly-api.ps1 every 10 min., as the service account, "Run whether user is logged on or not". So that two runs never overlap — Settings → If the task is already running: Do not start a new instance.

5.2. Python

import json
import subprocess

CLI = r"C:\Program Files\Blackoutly\BlackoutlyCli.exe"


def blackoutly(command: str, *paths: str, review_dir: str, output_dir: str | None = None) -> list[dict]:
    args = [CLI, command, *paths, "--review-dir", review_dir, "--json"]
    if output_dir:
        args += ["-o", output_dir]
    proc = subprocess.run(args, capture_output=True, encoding="utf-8", timeout=3600)
    if proc.returncode == 3:
        raise RuntimeError("No Blackoutly Enterprise license")
    if proc.returncode not in (0, 2):
        raise RuntimeError(proc.stderr)
    return json.loads(proc.stdout)


for row in blackoutly("apply", r"D:\Integration\inbox", review_dir=r"\\srv\review", output_dir=r"D:\Integration\outbox"):
    if row["status"] == "saved":
        print("OK", row["output"], row["output_sha256"])

5.3. C# (.NET)

using System.Diagnostics;
using System.Text.Json;

var psi = new ProcessStartInfo(@"C:\Program Files\Blackoutly\BlackoutlyCli.exe")
{
    RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false,
    StandardOutputEncoding = System.Text.Encoding.UTF8,
};
foreach (var a in new[] { "apply", @"D:\Integration\inbox", "--review-dir", @"\\srv\review", "-o", @"D:\Integration\outbox", "--json" })
    psi.ArgumentList.Add(a);

using var p = Process.Start(psi)!;
string json = p.StandardOutput.ReadToEnd();
p.WaitForExit();
if (p.ExitCode == 3) throw new InvalidOperationException("A Blackoutly Enterprise license is needed");

foreach (var row in JsonDocument.Parse(json).RootElement.EnumerateArray())
    if (row.GetProperty("status").GetString() == "saved")
        Console.WriteLine(row.GetProperty("output").GetString());

5.4. One file from the command line (to try it)

"C:\Program Files\Blackoutly\BlackoutlyCli.exe" prepare D:\Test\claim.pdf
"C:\Program Files\Blackoutly\BlackoutlyCli.exe" apply   D:\Test\claim.pdf -o D:\Test\results

6. The review package (*.bo-review.json)

The file name is the first 16 characters of the file's SHA-256: 4f2a9c0e1b7d3a55.bo-review.json. The package is bound to the file's exact content: for a changed file (another SHA-256) the old package does not fit. The package holds no document text — only the marks' places, so it can be kept in a shared folder.

{
  "version": 1,
  "path": "\\\\server\\blackoutly\\inbox\\claim.pdf",
  "file_name": "claim.pdf",
  "sha256": "4f2a9c0e1b7d3a55…",
  "status": "approved",
  "profile": "court",
  "rotations": {"2": 90},
  "marks": [
    {"page": 0, "rects": [[72.0, 140.5, 188.2, 154.0]], "category": "name", "source": "auto",
     "state": "accepted", "confidence": "high", "key": "0:112:129:name", "basis": null},
    {"page": 1, "rects": [[60.0, 300.0, 300.0, 360.0]], "category": "manual", "source": "manual",
     "state": "accepted", "confidence": null, "key": null, "basis": "gdpr"}
  ],
  "seen": ["0:112:129:name"],
  "prepared_by": {"user": "peter", "computer": "PC-104", "ts": "2026-10-18T10:02:11+03:00"},
  "approved_by": {"user": "anna", "computer": "PC-117", "ts": "2026-10-18T13:45:40+03:00"},
  "submitted_marks": [ … ]
}
Field Meaning
status draft (prepared by prepare), submitted (submitted by an employee), approved (approved by another)
rotations Page No. (from 0) → rotation in degrees clockwise
marks[].rects Rectangles in points (1/72 inch), from the top left corner of the rotated page: x0, y0, x1, y1
marks[].state pending (not reviewed — will not be hidden), accepted (will be hidden), rejected
marks[].category name, personal_code, address… or the organisation's org:<id>; manual — marked by hand
marks[].basis The legal ground's ID from the rules file; null — by category, "-" — without a ground
prepared_by, approved_by Windows user, computer, time
submitted_marks The marks as A submitted them (as marks) — shows whether B changed them

Do not create or change packages with your own system: the format may change in new versions (version), and an approved_by changed by hand would bypass the four-eyes principle — a breach of the organisation's procedure (who really saved it is always in the audit log). The external system may only read the marks (e.g. how many were hidden); they are made through the program.

7. Security and tips

The other commands (info, suggest, redact) are for developers and diagnostics; do not use them for integration — redact --auto saves without a person's confirmation.