CLI and API
Use the devguard CLI to upload the output of your checks into evidence records on a schedule, or call the Evidence API directly from your own scripts.
Most evidence is a file that someone exported once. A year later the file is old, and nobody notices until an auditor asks. The devguard CLI runs the same export on a schedule. It uploads the output into the evidence record you name.
Each collector (one command in your configuration) replaces its own previous file on every push. The old file stays under Previous versions. If a file is not refreshed in time, it appears on Deadlines under Automated Evidence, and the owner role gets a notice.
This page has two parts:
- The CLI setup, from an empty repository to a scheduled job (steps 1 to 7).
- The Evidence API, for your own scripts that list, create or upload records.
Every command, flag and configuration field is in the CLI reference.
Who does what
Two people share the work: the owner of the evidence and the person who runs the build pipeline. An owner or an admin creates the token. Do the steps in this order.
| Step | Where | Who |
|---|---|---|
| Create the evidence record and link its controls | devguard | The owner of the evidence |
| Create an API token for the pipeline | devguard | An owner or an admin |
| Install the tool and sign in | A terminal | The person who runs the pipeline |
| Check the connection | devguard and the terminal | The same person |
| Commit the configuration and push once | The repository, a terminal | The person who runs the pipeline |
| Schedule the job | The pipeline | The person who runs the pipeline |
The setup is one file in your repository and one scheduled job. If the person who built it leaves and the job stops, the file expires and appears on Deadlines.
1. Create the evidence record
Create the record in Evidence and link it to its controls. The configuration uses the record's number, such as EV-42. The CLI only writes into records that exist. It never creates a record. If the number does not exist, the push fails before any command runs.
2. Create an API token for the pipeline
A pipeline cannot open a browser, so it needs a token. On your own computer you can skip this step, because signing in opens a browser.
Create an organization token under Settings, then Tokens. Give it read and write access to the Evidence area. An organization token belongs to the organization, so it keeps working when people leave. A personal token from Account, then Tokens, also works, but it stops when its creator leaves.
Store the token as a masked secret in your pipeline. Never put it in the repository. Replace it if anyone else sees it.
Watch out: a new token expires after 90 days unless you choose another date. An expired token stops the job without a warning, so choose a long expiry or never. A member's token does not work either: only owners, admins and organization tokens can push.
3. Install and sign in
The npm package needs Node 20 or newer. A runner without Node can use the Linux binaries (x64 and arm64) on each GitHub release. Check them against the SHA256SUMS file next to them.
npm install -g @devguardch/cli
devguard loginSigning in opens your browser. Choose the organization this machine may use and allow the connection. You do not paste anything into the terminal.
The login works for that one organization and stores no permanent secret. It stops after seven days without use. An admin can end it under Settings, Integrations, Connected apps. devguard logout ends it on this machine.
On a computer without a browser, run devguard login --key. It asks for an API token and hides what you type. You can also pass the token as devguard login --key dvgo_... or in the DEVGUARD_API_KEY variable.
Note the organization id that the login prints. It is not the name in the address bar. Your pipeline needs this id.
4. Check the connection
Before you set up the pipeline, you can check that the tool reaches devguard. The check on the CLI page in the app watches only your personal API tokens from Account, then Tokens. An organization token or a browser login never turns it green. Listen for a signal stays gray until you have a personal token.
- If you have no personal token, create one under Account, then Tokens.
- In the app, open Evidence, then CLI. Under Try it, choose Listen for a signal. The page waits for five minutes.
- On the machine that will push, run
devguard login --keyand paste the personal token. - The card shows Signal received and the token name. Nothing is uploaded and no record changes.
To check an organization token instead, run devguard evidence validate (step 6). Then look at its Last used date under Settings, then Tokens.

5. Describe what to collect
Run devguard init on your own computer. It asks a few questions and writes a first devguard.yml. You can also write the file by hand:
# CI runners have no stored login, so name the organization here.
# devguard login prints the id; it is not the slug from the URL.
organization: <your organization id>
collectors:
# The name is the file's identity. Every push replaces this
# collector's previous file, so the record holds one current file.
- name: npm-audit
command: npm audit --json
evidence: 42
# npm audit exits 1 as soon as it finds anything, so accept it.
allowExitCodes: [0, 1]
expiresAt: 7d
# The recipe form picks an installed scanner and fills in the
# command, the output file and the exit codes for you.
- name: vulnerability-scan
uses: scan
evidence: 57
expiresAt: 7d| Field | Required | Default | Notes |
|---|---|---|---|
name | yes | Lowercase slug, unique in the file. It identifies the file. A new name starts a new series. | |
command | yes | The command to run. Leave it out with uses: scan. | |
evidence | yes | 42, EV-42 or ev-042. The record must already exist. | |
output | no | stdout | A file path when the command writes to disk. An empty output fails the collector. |
expiresAt | no | 30d | How long the file stays current, or never. |
timeout | no | 10m | At most 24d. |
allowExitCodes | no | [0] | Set [0, 1] for scanners that exit 1 on findings. |
allowSecrets | no | false | Lets this collector upload output that matches the secret scan. |
The CLI rejects unknown fields, so a typo fails at once. Durations use seconds, minutes, hours, days and weeks. Use 12w for a quarter, because the CLI does not know months or years. Add the .devguard folder to your gitignore. It holds the files before upload. The other fields and the scan recipe are in the configuration reference.
6. Push once by hand
devguard evidence validate # config, credentials and every EV number
devguard evidence push --dry-run # runs everything, uploads nothing
devguard evidence pushvalidate reads the configuration, checks your login and checks that every evidence number exists. It runs nothing. A dry run runs the collectors and the secret scan, but uploads nothing.
After the push, the record's Attachments show one file per collector, with an Automated badge and the collector name.
7. Run it on a schedule
Any tool that runs a command on a schedule works: a CI service, a cron job on a server or a scheduled task. GitHub Actions is only the shortest example.
name: evidence
on:
schedule: [{ cron: '17 3 * * *' }]
workflow_dispatch:
jobs:
push-evidence:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22 }
- run: npx @devguardch/cli evidence push
env:
DEVGUARD_API_KEY: ${{ secrets.DEVGUARD_API_KEY }}Make the expiry longer than the time between two runs. Otherwise the file expires between two good pushes. The runner must reach devguard and its file storage, because the file goes directly to storage.
What leaves your machine
The command runs on your machine or your runner, never on devguard's servers. Only the output file you name is uploaded. Your repository, cloud accounts and passwords stay where they are.
Before upload, the CLI checks each file for secrets, such as cloud keys, private keys, tokens and long random strings. If it finds one, it stops the push and prints a masked report. If a collector's output must contain such a string, set allowSecrets: true on that collector. devguard evidence push --allow-secrets turns the check off for one whole run.
What a push changes, and what it does not
- The collector name identifies the file. A push replaces only that collector's previous file. Files uploaded by hand and files from other collectors stay.
- The replaced file stays under Previous versions. If you rename a collector, a new series starts, and nothing refreshes the old file.
- Each file has an expiry date. When it passes, the file appears on Deadlines under Automated Evidence, and the owner role gets a notice.
- A push does not change the record's status, control coverage or review date. A record on To do stays on To do. The owner sets the status and the approver reviews.
- If a collector fails, it uploads nothing and its previous file stays. The other collectors still push, and the run ends with exit code 1.
Evidence API
A runner without Node, or your own script, can call the REST API directly. The base URL, authentication and all evidence endpoints are in the API section of the Evidence page. This section lists the request fields and shows examples.
List records
| Parameter | Type | Notes |
|---|---|---|
page | number | From 1. Default 1. |
limit | number | 5 to 100. Default 100. |
filterByName | string | Title, case-insensitive, or an EV-n code. |
status | enum | TODO, IN_PROGRESS, DONE, FAILED or ARCHIVED. Without it, all statuses are returned. |
filterByArchived | enum | active or archived. Ignored when status is set. |
categoryId | string | One category. |
filterByLabels | string, repeatable | Records with any of these label ids: filterByLabels=a&filterByLabels=b. |
filters | JSON | The list toolbar filters. See the example on the Evidence page. |
sort | JSON | [{"id":"title","desc":false}], at most 5 entries. Ids: shortId, title, description, status, nextReviewAt, owner, approver, category. |
sortColumn, sortDirection | string, asc or desc | One sort id and its direction. Ignored when sort is set; sortColumn alone is ignored. |
Create and update records
| Field | Create | Update | Type and limits |
|---|---|---|---|
title | required | required | string, 2 to 255 |
ownerId | required | optional | business role id, or null on update |
approverId | required | optional | business role id, or null on update |
description | optional | optional | string or null |
status | optional, TODO | optional | TODO, IN_PROGRESS, DONE, FAILED or ARCHIVED |
categoryId | optional | optional | category id or null |
reviewScheduleId | optional | optional | schedule id or null |
continuousReview | optional, false | optional | boolean; not allowed together with a schedule |
labels | optional | optional | label ids; if you leave them out on update, they stay; if you send them, they replace the old ones |
devguard sets the review dates and the approval fields itself. See the API section of the Evidence page.
DG_URL=https://app.devguard.ch
API="$DG_URL/api"
AUTH="Authorization: Bearer $DG_TOKEN"
curl -sS -X POST "$API/evidence/" \
-H "$AUTH" -H 'Content-Type: application/json' \
-d "$(jq -n --arg org "$DG_ORG" \
--arg o "$OWNER_ROLE" --arg a "$APPROVER_ROLE" '{
organizationId: $org,
title: "Quarterly access review",
ownerId: $o, approverId: $a }')" \
| jq '{id, shortId, status}'Upload a file
A file arrives in three calls, the same ones the CLI uses: prepare the upload, send the file to storage, then finish.
| Field | Required | Type and limits |
|---|---|---|
organizationId | yes | the organization id |
evidenceId | yes | the record's id, from GET /evidence/short/{shortId}/. The endpoint also accepts assessmentId instead, for vendor assessments, but then not sourceKey |
originalName | yes | file name |
mimeType | yes | content type; storage checks it |
size | yes | bytes, 1 to 209,715,200 |
sha256 | no | 64 lowercase hex characters; storage checks the bytes against it |
sourceKey | no | lowercase slug, at most 100; replaces the previous file with the same key, like a collector name |
issuedAt, expiresAt | no | ISO date or null; expiresAt is the expiry date |
documentKind | no | SOC2, ISO27001, PENTEST, DPA, AOC, BAA or OTHER |
title, description | no | string |
POST /evidence/files/prepare-upload/creates a hidden draft. It returns afileId, a storageurlandfields.- Within ten minutes, send a multipart POST to that
url. Put all entries offieldsfirst, in the given order, and thefilepart last. Do not send the bearer token. - Call
POST /evidence/files/finalize-upload/withorganizationIdandfileId. devguard checks that the file exists and is not empty, saves its real size and lists it. With asourceKey, the previous file with that key moves to Previous versions.
If step 2 fails, DELETE /evidence/files/{fileId}/?organizationId=… deletes the draft.
The script below follows the CLI's own steps. Unlike the CLI, sourceKey is optional, so a one-off upload adds a file instead of replacing one.
#!/usr/bin/env bash
# DG_URL=https://app.devguard.ch DG_TOKEN=dvg_... DG_ORG=<org id>
# ./upload.sh <number, e.g. 42> <file> [sourceKey]
set -euo pipefail
ev=$1 f=$2 key=${3:-}
api="$DG_URL/api" org=$DG_ORG
c=(curl --fail-with-body -sS)
auth=(-H "authorization: Bearer $DG_TOKEN")
json=(-H 'content-type: application/json')
name=$(basename "$f")
ext=$(tr '[:upper:]' '[:lower:]' <<<"${name##*.}")
case $ext in
json) mime=application/json ;;
sarif) mime=application/sarif+json ;;
xml) mime=application/xml ;;
txt) mime=text/plain ;;
csv) mime=text/csv ;;
html) mime=text/html ;;
*) mime=application/octet-stream ;;
esac
# 0. record number to evidence id
id=$("${c[@]}" "${auth[@]}" \
"$api/evidence/short/$ev/?organizationId=$org" | jq -r .id)
# 1. prepare: draft row and presigned POST
body=$(jq -n --arg org "$org" --arg ev "$id" \
--arg name "$name" --arg mime "$mime" \
--argjson size "$(wc -c <"$f" | tr -d ' ')" \
--arg sha "$(shasum -a 256 "$f" | cut -d' ' -f1)" \
--arg key "$key" --arg now "$(date -u +%FT%TZ)" \
'{organizationId: $org, evidenceId: $ev,
originalName: $name, mimeType: $mime, size: $size,
sha256: $sha, issuedAt: $now, expiresAt: null}
+ if $key == "" then {} else {sourceKey: $key} end')
prep=$("${c[@]}" "${auth[@]}" "${json[@]}" -d "$body" \
"$api/evidence/files/prepare-upload/")
fid=$(jq -r .fileId <<<"$prep")
# 2. storage: every field in order, the file last, no bearer
form=()
while IFS= read -r kv; do form+=(--form-string "$kv"); done \
< <(jq -r '.fields | to_entries[] | "\(.key)=\(.value)"' \
<<<"$prep")
"${c[@]}" "${form[@]}" \
-F "file=@$f;type=$mime;filename=$name" \
"$(jq -r .url <<<"$prep")" || {
"${c[@]}" -X DELETE "${auth[@]}" \
"$api/evidence/files/$fid/?organizationId=$org"
exit 1
}
# 3. finalize: devguard checks the object and lists the file
"${c[@]}" "${auth[@]}" "${json[@]}" \
-d "$(jq -n --arg o "$org" --arg f "$fid" \
'{organizationId: $o, fileId: $f}')" \
"$api/evidence/files/finalize-upload/"Use --form-string, not -F, for the fields. With -F, curl reads a value that starts with @ or < as a file name.
Commands
| Command | What it does |
|---|---|
devguard login | Opens your browser to sign in and stores the login for the target address. --key verifies and stores an API token instead. |
devguard logout | Ends the login on this machine and deletes it. |
devguard init | Asks questions and writes a first devguard.yml. |
devguard evidence validate | Checks only. Nothing runs and nothing is uploaded. |
devguard evidence push | Runs collectors, scans for secrets, uploads and replaces. --dry-run runs everything but uploads nothing. |
devguard scan --evidence 42 | Runs an installed scanner and pushes its report, without a configuration file. |
Connection settings are read in this order: the --url and --key flags, then DEVGUARD_API_URL and DEVGUARD_API_KEY, then the stored login, then https://app.devguard.ch.
The organization comes from the --org flag, then organization: in the configuration, then the default from the login. For push, validate and scan, this must be the organization id. Only login and init accept the name from the address bar. A browser login works for one organization, so a configuration with another one is rejected before anything is sent. All flags are in the commands reference.
When a push fails
| Message | Cause |
|---|---|
Authentication failed | The API token is wrong or expired. Fix DEVGUARD_API_KEY in the pipeline, or run devguard login --key. |
Not logged in | No credential at all: run devguard login, set DEVGUARD_API_KEY, or pass --key. |
This login for ... has expired or was revoked | The browser login was disconnected, or went seven days without use. Run devguard login again. |
EV-42 does not exist in this organization | Create the record in devguard first. |
This login has no membership in organization | Usually the slug was used where the organization id is required. |
Access denied | The account behind the credentials is a member. Only owners, admins and organization tokens can push. |
Your role can read this organization but not change it | The account is an auditor, which can only read. |
command produced no output on stdout | The command wrote nothing. Empty artifacts are refused. |
command exited 1 (allowed: 0) | The scanner exits 1 on findings. Add allowExitCodes: [0, 1] to that collector. |
not attempted: the login is no longer usable | The browser login expired or was revoked mid-run, so the remaining collectors were skipped. Run devguard login and push again. |
How is this guide?