banshee is under active development: collectors will keep changing, and a version you publish will likely need adjustments. For stable use, wait for version 1.0.0.

Services

banshee/services-collection/services/veeam/https/

jobs

The last result, state and age of the last success of each job of a Veeam Backup & Replication 12 or later, by its REST API.

  • services
  • backup
  • veeam
  • vbr
  • jobs
  • https
  • api

by banshee1 installation2 versions

v2Latest

Published on October 28, 2026. One naming convention across collections; documentation completed.

Keen 1.0.0 · 123 lines
// The jobs of a Veeam Backup & Replication server, by its REST API (12 and later): for each job, the result of
// its last run, its state, and the age of its last run that succeeded (Success or Warning). A job that fails,
// or that no longer runs, says it.
// Reads POST /api/oauth2/token, then GET /api/v1/jobs/states, then, for each job not disabled, GET
// /api/v1/sessions for its newest session that ended in Success, and in Warning.
// The address of the API is the backup server's, the host's: https://<host:address>:9419/api/.
need('protocol:http');
$address = need('host:address');
$user = need('protocol:http:user');
$password = need('protocol:http:password');
$port = variable('veeam:port', 'integer', array(
    'label'   => 'API port',
    'help'    => 'The port of the Veeam Backup & Replication REST API: 9419, unless it was changed at install.',
    'default' => 9419,
));

per('job', 'Jobs');
output('result', 'text', array('label' => 'Last result', 'per' => 'job'));
output('state', 'text', array('label' => 'State', 'per' => 'job'));
output('success-age', 'number', array('label' => 'Age of the last success', 'unit' => 'h', 'per' => 'job'));

watch.state('failed', 'Job failed', 'result', array('warning' => array('warning'), 'critical' => array('failed')));
watch.above('old', 'Last success too old', 'success-age', array('warning' => 30, 'critical' => 54));
watch.state('disabled', 'Job disabled', 'state', array('warning' => array('disabled'), 'enabled' => false));
watch.missing('missing', 'Job missing', 'result', array('enabled' => false));

// The revision of the API this collector is written for: 1.1-rev0, that of Veeam Backup & Replication 12.0,
// served by 12.x and deprecated, still served, from 13.0.
function api_headers($token) {
    return array('x-api-version' => '1.1-rev0', 'Authorization' => 'Bearer ' . $token);
}

// Log in: the account goes in a form, which makes the answer secret, and its access token with it. The token is
// kept in the cache, encrypted on the poller, ten minutes, under the fifteen it lives.
function login($base, $user, $password) {
    $body = url.query(array('grant_type' => 'password', 'username' => $user, 'password' => $password));
    $answer = http.post($base . '/api/oauth2/token', $body, headers=array('x-api-version' => '1.1-rev0',
        'Content-Type' => 'application/x-www-form-urlencoded'));
    if ($answer['status'] == 400 || $answer['status'] == 401 || $answer['status'] == 403) {
        fail('misconfigured', 'Veeam refuses the account, code ' . $answer['status']);
    }
    if ($answer['status'] != 200) {
        fail('unreachable', 'Veeam answers the login with code ' . $answer['status'],
            'Veeam Backup & Replication serves its REST API from version 12.');
    }
    $session = json.read($answer['body']);
    cache.set('token', $session['access_token'], 600);
    return $session['access_token'];
}

// One GET with the token kept in the cache; a token Veeam no longer knows (expired, the service restarted)
// answers 401, and the collector logs in again, once.
function api_get($base, $path, $user, $password) {
    if (cache.has('token')) {
        $token = cache.get('token');
    } else {
        $token = login($base, $user, $password);
    }
    $answer = http.get($base . $path, headers=api_headers($token));
    if ($answer['status'] == 401) {
        $token = login($base, $user, $password);
        $answer = http.get($base . $path, headers=api_headers($token));
    }
    if ($answer['status'] == 403) {
        fail('misconfigured', 'The account may not read ' . $path . ', code 403');
    }
    if ($answer['status'] != 200) {
        fail('unreachable', 'Veeam answers ' . $path . ' with code ' . $answer['status']);
    }
    return json.read($answer['body']);
}

$base = 'https://' . $address . ':' . $port;

// The jobs, a page of 200 at a time: {"data": [{"id": "…", "name": "Daily VMs", "type": "Backup", "status":
// "inactive", "lastResult": "Success", "lastRun": "2026-10-26T01:00:03.123+01:00", …}], "pagination":
// {"total": 12, "count": 12, "skip": 0, "limit": 200}}. status is running, inactive or disabled; lastResult
// None (never run), Success, Warning or Failed. Two jobs of one name each keep their own, the second carrying
// its id.
$jobs = array();
$skip = 0;
$total = 1;
while ($skip < $total) {
    $page = api_get($base, '/api/v1/jobs/states?' . url.query(array('skip' => $skip, 'limit' => 200)), $user, $password);
    foreach ($page['data'] as $job) {
        $jobs = list.add($jobs, $job);
    }
    $total = $page['pagination']['total'];
    $skip = $skip + 200;
}

$now = time.now();
$seen = array();
foreach ($jobs as $job) {
    $name = $job['name'];
    if (list.has($seen, $name)) {
        $name = $name . ' #' . $job['id'];
    }
    $seen[$name] = true;
    value('result', text.lower($job['lastResult']), $name);
    value('state', $job['status'], $name);
    if ($job['status'] == 'disabled') {
        continue;
    }

    // The newest session of the job that ended in Success, then in Warning: the time of the last success is
    // the later of their ends, written 2026-10-26T01:00:03.1234567+01:00.
    $last = '';
    foreach (array('Success', 'Warning') as $result) {
        $query = url.query(array('jobIdFilter' => $job['id'], 'resultFilter' => $result,
            'orderColumn' => 'CreationTime', 'orderAsc' => 'false', 'limit' => 1));
        $sessions = api_get($base, '/api/v1/sessions?' . $query, $user, $password);
        foreach ($sessions['data'] as $session) {
            $end = time.parse(list.get($session, 'endTime', ''), 'iso8601');
            if ($last == '' || $end > $last) {
                $last = $end;
            }
        }
    }
    if ($last != '') {
        value('success-age', number.round(($now - $last) / 3600, 1), $name);
    }
}