# Vendor SLA Attainment and Credit-Claim Report (`kingii98/vendor-sla-attainment-and-credit-claim-report`) Actor

Turn a vendor's public status page into one number for the billing period. For each vendor: the uptime percentage, the downtime minutes split by severity, the incident count, the longest outage, the breach flag against your contracted threshold, the claim

- **URL**: https://apify.com/kingii98/vendor-sla-attainment-and-credit-claim-report.md
- **Developed by:** [kingii98](https://apify.com/kingii98) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $10.00 / 1,000 run\_starts

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Vendor SLA Attainment and Credit-Claim Report

Get one number for each vendor at the end of a billing period.

This Actor reads a vendor's **public status page** and computes the uptime that
the vendor actually delivered over your billing period. It then compares that
number against the uptime your contract promises, and shows the minutes you can
claim back when the vendor falls short.

It is built for the person who approves a renewal or files a credit claim, not
for the person who watches a dashboard. There is no alert and no schedule to
babysit. You run it once at the end of the month or the quarter, and you get a
row for each vendor with the number and the evidence behind it.

### What you get for each vendor

One dataset record for each vendor and period:

| Field | Meaning |
| --- | --- |
| `uptime_pct` | The delivered uptime over the period, to six decimal places. |
| `sla_threshold_pct` | The uptime your contract promises for that vendor. |
| `breach` | `true` when the downtime is larger than the allowed budget. |
| `downtime_minutes` | Total downtime minutes inside the period. |
| `downtime_minutes_critical` / `_major` / `_minor` / `_none` | The same downtime split by severity. |
| `allowed_downtime_minutes` | The budget the threshold allows over this period. |
| `credit_claim_minutes` | The minutes above that budget. This is what you claim. |
| `incident_count` | Incidents that touch the period, after your exclusions. |
| `counted_incident_count` | Of those, the ones that removed uptime. |
| `longest_outage_minutes` | The longest single stretch of downtime. |
| `longest_outage_incident` | The incident that drove that stretch. |
| `incidents` | The incident list that produced the number, with the minutes each one spent in the period. |
| `history_coverage` | `complete`, or `truncated` when the vendor publishes no further back. |

A `run_summary` record adds the counts for the whole run: how many vendors were
measured, how many breached, and the total claimable minutes.

### How the number is computed

Three rules decide whether the number survives an audit.

1. **Concurrent incidents share one clock.** Two overlapping outages of 3 hours
   and 4 hours are 5 hours of downtime, not 7. Every interval set is merged
   before it is measured.
2. **An incident is clipped to the period.** An outage that began last month
   spends only its minutes of *this* month in this report.
3. **Severity decides what counts.** By default only a `critical` or a `major`
   incident removes uptime, because most contracts call a `minor` incident
   degradation and not downtime. Change `downtimeImpacts` to match your
   contract. Planned maintenance never counts.

The breach flag compares **seconds against the allowed budget**, and not the
rounded percentage, so a vendor can never slip under the threshold through
display rounding. The budget itself is computed with exact decimal arithmetic,
which matters at 99.99 percent.

An incident that names only components you excluded is dropped. An incident
that names no component at all is kept, because the status page could not
attribute it and dropping it would understate the downtime.

### Which status pages work

Public **Statuspage** and **Instatus** JSON feeds. No account and no API key.

The usual URL is:

```
https://status.<vendor>.com/api/v2/incidents.json
```

Some vendors use a different hostname, for example
`https://www.githubstatus.com/api/v2/incidents.json`. A
`/api/v2/summary.json` URL and a `/history.json` URL also work.

#### Reaching behind the 50-incident cap

The public Statuspage API answers with the **50 most recent incidents** and has
no page parameter. For a busy vendor and a long period, those 50 incidents may
not reach back to your period start.

When that happens, this Actor reads the `/history.json` pages of the same
status page, which hold three months each, until the period is covered or
`maxHistoryPages` is reached. It keeps the API record of any incident that
appears in both, because the API carries exact timestamps and the history page
only carries minutes.

If the vendor publishes no history that far back, the report says
`history_coverage: "truncated"` and names the oldest instant it could confirm.
The number is still produced; you simply know it may understate the downtime.

### Input

Every field has a default, so a run with an empty input works.

| Field | Default | Meaning |
| --- | --- | --- |
| `vendors` | three public status pages | 1 to 50 entries of `{name, status_json_url, sla_threshold_pct, excluded_components}`. A plain URL string also works. |
| `periodStart` | empty | The first instant of the period. A date begins at 00:00 UTC. |
| `periodEnd` | empty | The last instant. A date such as `2026-08-31` covers the whole of that day. |
| `downtimeImpacts` | `["critical","major"]` | The severities that remove uptime. |
| `maxHistoryPages` | `3` | History pages read for each vendor behind the API cap. `0` reads the given URL alone. |
| `maxIncidentsPerReport` | `200` | Incident records kept in the evidence list. The longest ones are kept. |
| `requestTimeoutSeconds` | `30` | Timeout for each read. |
| `concurrency` | `5` | Vendors read at the same time. |
| `maxRedirects` | `5` | Redirects followed for one read. Every hop is checked again. |
| `maxResponseBytes` | `5000000` | Size cap for one JSON answer. |

**Leave both period fields empty** to report the last complete calendar month.
A monthly schedule then stays correct for ever, with no input to edit.

The period must be at most **400 days**. A period that has not finished yet is
measured up to now, and the report sets `period_complete: false`.

#### Example

```json
{
  "vendors": [
    {
      "name": "GitHub",
      "status_json_url": "https://www.githubstatus.com/api/v2/incidents.json",
      "sla_threshold_pct": 99.9,
      "excluded_components": ["Visit www.githubstatus.com for more information"]
    },
    {
      "name": "Cloudflare",
      "status_json_url": "https://www.cloudflarestatus.com/api/v2/incidents.json",
      "sla_threshold_pct": 99.99,
      "excluded_components": []
    }
  ],
  "periodStart": "2026-08-01",
  "periodEnd": "2026-08-31",
  "downtimeImpacts": ["critical", "major"]
}
```

### Pricing

This Actor is billed per event.

| Event | Charged | Counted |
| --- | --- | --- |
| `run_start` | once for each run | The container start and the store init. Charged even when a run is stopped early. |
| `vendor_period_report` | once for each vendor-period record measured | The history reads and the interval arithmetic for one vendor and one period. |
| `breach_evidence_pack` | once for each vendor below its threshold | The extra incident roll-up that supports the credit claim. |

A vendor that could not be read, an input entry that could not be read, and a
vendor left out by the run's charge limit are **not** charged, because no report
was produced for them. They still appear in the dataset so that nothing is
missing from your review.

The run never reads more vendors than its maximum total charge allows.

### Limits and safety

- 50 vendors for each run, and a period of at most 400 days.
- HTTP and HTTPS only. A URL with credentials is refused.
- A target that resolves to a private, loopback, link-local or otherwise
  reserved address is refused, and every redirect hop is checked again.
- The redirect chain, the response size and the request timeout are all bounded.
- No browser, no proxy, no paid API, no account and no stored state. Each run
  computes the result from the published history, so it is re-runnable.

### When something goes wrong

A business verdict is never a failed run. A vendor below its threshold, a
status page that answers HTTP 503, a payload that is not a status page and an
input that breaks a bound all end with a **succeeded** run that carries the
finding in the dataset and in the status message. A failed run means this Actor
itself malfunctioned.

### Development

```bash
uv sync
uv run pytest
uv run ruff check .
```

# Actor input Schema

## `vendors` (type: `array`):

1 to 50 vendors. Each entry is an object with 'name', 'status\_json\_url', 'sla\_threshold\_pct' and 'excluded\_components'. The URL must be a public Statuspage or Instatus JSON feed, normally https://status.<vendor>.com/api/v2/incidents.json. A plain URL string is also accepted, and then the hostname becomes the vendor name. The threshold defaults to 99.9 percent. 'excluded\_components' names the components that your contract does not cover, for example a status page or a docs site. An entry this Actor cannot read gets its own row and does not stop the run.

## `periodStart` (type: `string`):

The first instant of the billing period, as a date such as 2026-08-01 or a timestamp such as 2026-08-01T00:00:00Z. A date begins at 00:00 UTC. Leave both period fields empty to report the last complete calendar month, which keeps a monthly schedule correct for ever.

## `periodEnd` (type: `string`):

The last instant of the billing period. A date such as 2026-08-31 covers the whole of that day, so the period ends at 2026-09-01T00:00:00Z. A timestamp is used exactly. The period must be at most 400 days. A period that has not finished is measured up to now, and the report says so.

## `downtimeImpacts` (type: `array`):

An incident of one of these severities removes its minutes from the uptime number. Most contracts treat only a major or a critical incident as downtime, and call a minor incident degradation. An incident of any other severity is still reported and still carries its minutes, but it does not change the uptime number. Planned maintenance never counts.

## `maxHistoryPages` (type: `integer`):

The public Statuspage API answers with the 50 most recent incidents only, and it states no retention window, so a period that reaches behind its oldest incident is reported as partial coverage. The run then adds the history pages of the same status page, which hold three months each, until the period is covered. Raise this for a period longer than nine months. Set 0 to read the given URL alone. Not every status page serves a history page; when it does not, the report says so and still gives the number.

## `maxIncidentsPerReport` (type: `integer`):

The largest number of incident records kept in the evidence list of one vendor row. The longest incidents are kept first, because they carry the claim. The counts and the uptime number always use every incident.

## `requestTimeoutSeconds` (type: `integer`):

Timeout for each JSON read. One vendor needs one to four reads.

## `concurrency` (type: `integer`):

The largest number of vendors read at the same time. A public status page refuses a burst, so a small number is safer.

## `maxRedirects` (type: `integer`):

The largest number of redirects followed for one read. Every hop is checked again against the private-address rule.

## `maxResponseBytes` (type: `integer`):

A JSON answer larger than this cap is refused, and the vendor is reported as unreadable.

## Actor input object example

```json
{
  "vendors": [
    {
      "name": "GitHub",
      "status_json_url": "https://www.githubstatus.com/api/v2/incidents.json",
      "sla_threshold_pct": 99.9,
      "excluded_components": []
    },
    {
      "name": "Cloudflare",
      "status_json_url": "https://www.cloudflarestatus.com/api/v2/incidents.json",
      "sla_threshold_pct": 99.99,
      "excluded_components": []
    },
    {
      "name": "OpenAI",
      "status_json_url": "https://status.openai.com/api/v2/incidents.json",
      "sla_threshold_pct": 99.9,
      "excluded_components": []
    }
  ],
  "periodStart": "",
  "periodEnd": "",
  "downtimeImpacts": [
    "critical",
    "major"
  ],
  "maxHistoryPages": 3,
  "maxIncidentsPerReport": 200,
  "requestTimeoutSeconds": 30,
  "concurrency": 5,
  "maxRedirects": 5,
  "maxResponseBytes": 5000000
}
```

# Actor output Schema

## `dataset` (type: `string`):

One record for each vendor and period, with the uptime percentage, the downtime minutes by severity, the incident count, the longest outage, the breach flag, the claimable minutes and the incident list that produced the number, plus one run-summary record.

## `breaches` (type: `string`):

The vendor rows whose uptime falls below the contracted threshold. These are the rows that support a credit claim.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "vendors": [
        {
            "name": "GitHub",
            "status_json_url": "https://www.githubstatus.com/api/v2/incidents.json",
            "sla_threshold_pct": 99.9,
            "excluded_components": []
        },
        {
            "name": "Cloudflare",
            "status_json_url": "https://www.cloudflarestatus.com/api/v2/incidents.json",
            "sla_threshold_pct": 99.99,
            "excluded_components": []
        },
        {
            "name": "OpenAI",
            "status_json_url": "https://status.openai.com/api/v2/incidents.json",
            "sla_threshold_pct": 99.9,
            "excluded_components": []
        }
    ],
    "periodStart": "",
    "periodEnd": "",
    "downtimeImpacts": [
        "critical",
        "major"
    ],
    "maxHistoryPages": 3,
    "maxIncidentsPerReport": 200,
    "requestTimeoutSeconds": 30,
    "concurrency": 5,
    "maxRedirects": 5,
    "maxResponseBytes": 5000000
};

// Run the Actor and wait for it to finish
const run = await client.actor("kingii98/vendor-sla-attainment-and-credit-claim-report").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "vendors": [
        {
            "name": "GitHub",
            "status_json_url": "https://www.githubstatus.com/api/v2/incidents.json",
            "sla_threshold_pct": 99.9,
            "excluded_components": [],
        },
        {
            "name": "Cloudflare",
            "status_json_url": "https://www.cloudflarestatus.com/api/v2/incidents.json",
            "sla_threshold_pct": 99.99,
            "excluded_components": [],
        },
        {
            "name": "OpenAI",
            "status_json_url": "https://status.openai.com/api/v2/incidents.json",
            "sla_threshold_pct": 99.9,
            "excluded_components": [],
        },
    ],
    "periodStart": "",
    "periodEnd": "",
    "downtimeImpacts": [
        "critical",
        "major",
    ],
    "maxHistoryPages": 3,
    "maxIncidentsPerReport": 200,
    "requestTimeoutSeconds": 30,
    "concurrency": 5,
    "maxRedirects": 5,
    "maxResponseBytes": 5000000,
}

# Run the Actor and wait for it to finish
run = client.actor("kingii98/vendor-sla-attainment-and-credit-claim-report").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "vendors": [
    {
      "name": "GitHub",
      "status_json_url": "https://www.githubstatus.com/api/v2/incidents.json",
      "sla_threshold_pct": 99.9,
      "excluded_components": []
    },
    {
      "name": "Cloudflare",
      "status_json_url": "https://www.cloudflarestatus.com/api/v2/incidents.json",
      "sla_threshold_pct": 99.99,
      "excluded_components": []
    },
    {
      "name": "OpenAI",
      "status_json_url": "https://status.openai.com/api/v2/incidents.json",
      "sla_threshold_pct": 99.9,
      "excluded_components": []
    }
  ],
  "periodStart": "",
  "periodEnd": "",
  "downtimeImpacts": [
    "critical",
    "major"
  ],
  "maxHistoryPages": 3,
  "maxIncidentsPerReport": 200,
  "requestTimeoutSeconds": 30,
  "concurrency": 5,
  "maxRedirects": 5,
  "maxResponseBytes": 5000000
}' |
apify call kingii98/vendor-sla-attainment-and-credit-claim-report --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,kingii98/vendor-sla-attainment-and-credit-claim-report"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/2bZPiLMkSRx4U91An/builds/qimGkHWwYFdxNe8f5/openapi.json
