Documentation

Quickstart

Paging incident history, and reading uptime without dropping its denominator. Both are written out in full because both are places a caller gets a plausible wrong answer rather than an error.

Walking the history

limit stops at 200 and one venue having a bad week exceeds that on its own, so the whole history is a walk. The cursor is before — a timestamp, not an offset, because new incidents are inserted at the front of the ordering and an offset would skip a row every time one opened mid-walk.

package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"strconv"
	"time"
)

const base = "https://api.up24.app"

type incident struct {
	Slug       string `json:"slug"`
	Severity   string `json:"severity"`
	DurationMs int64  `json:"durationMs"`
	StartedAt  string `json:"startedAt"`
	ID         int64  `json:"id"`
}

// Walks the cursor rather than raising limit, which stops at 200.
func incidents(venue string, days int) ([]incident, error) {
	var all []incident
	before, beforeID := "", int64(0)

	for {
		q := url.Values{}
		q.Set("venue", venue)
		q.Set("days", strconv.Itoa(days))
		q.Set("limit", "200")
		if before != "" {
			q.Set("before", before)
			q.Set("beforeId", strconv.FormatInt(beforeID, 10))
		}

		res, err := http.Get(base + "/v1/incidents?" + q.Encode())
		if err != nil {
			return nil, err
		}

		// The one status worth handling: the API says when to come back.
		if res.StatusCode == http.StatusTooManyRequests {
			wait, _ := strconv.Atoi(res.Header.Get("retry-after"))
			res.Body.Close()
			time.Sleep(time.Duration(max(wait, 5)) * time.Second)
			continue
		}

		var body struct {
			Incidents []incident `json:"incidents"`
		}
		err = json.NewDecoder(res.Body).Decode(&body)
		res.Body.Close()
		if err != nil {
			return nil, err
		}

		all = append(all, body.Incidents...)
		// A short page is the end. The cursor is the last row's start and id,
		// never an offset: new incidents are inserted at the front, and
		// incidents opened in one tick share a start.
		if len(body.Incidents) < 200 {
			return all, nil
		}
		last := body.Incidents[len(body.Incidents)-1]
		before, beforeID = last.StartedAt, last.ID
	}
}

func main() {
	all, err := incidents("binance", 90)
	if err != nil {
		panic(err)
	}
	for _, i := range all {
		fmt.Println(i.Slug, i.Severity, i.DurationMs)
	}
}

The one status worth handling is 429: the API says when to come back in retry-after, and that header is the whole of the backoff you need. Nothing else here is retryable in a way you can guess at.

Quoting uptime honestly

Every number here is a measurement with a denominator attached. “99.98% uptime” without the check count or the window is not a claim up24 makes anywhere on this site, and anything named typical is a median of daily percentiles rather than the window's own — percentiles do not compose.

package main

import (
	"encoding/json"
	"fmt"
	"net/http"
)

func main() {
	res, err := http.Get("https://api.up24.app/v1/reliability?days=90")
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()

	var body struct {
		Venues []struct {
			Name    string `json:"name"`
			Summary struct {
				UptimePct    *float64 `json:"uptimePct"`
				Checks       int64    `json:"checks"`
				MeasuredDays int      `json:"measuredDays"`
				Days         int      `json:"days"`
				TypicalP95Ms *int     `json:"typicalP95Ms"`
			} `json:"summary"`
		} `json:"venues"`
	}
	if err := json.NewDecoder(res.Body).Decode(&body); err != nil {
		panic(err)
	}

	for _, v := range body.Venues {
		s := v.Summary
		// measuredDays beside days on purpose: a venue watched for eight days
		// of ninety has not been 99.99% for a quarter.
		fmt.Printf("%s: %.2f%% of %d checks over %d/%d days\n",
			v.Name, *s.UptimePct, s.Checks, s.MeasuredDays, s.Days)
	}
}

measuredDays is printed beside days on purpose: a venue watched for eight days of ninety has not been 99.99% for a quarter, and a dashboard that drops the first number is publishing a claim the data does not support.