Skip to main content

StageRage Partner API

This guide explains how to call the venue-score API. It returns aggregate crew review scores for a venue. All requests must be signed with HMAC-SHA256 using the credentials issued to you (a Key ID and a Secret).

1. Credentials

You receive a Key ID (public, sent with each request) and a Secret (private, used to compute the signature — never sent over the wire). The secret is shown once at creation; store it securely. If lost, rotate the key to get a new one.

2. The endpoint

GET https://stagerage.online/api/v1/public/venue-score
      ?name=<venue name>      (required)
      &city=<city>            (recommended)
      &country=<country>      (recommended)

Each parameter may appear once. Without a city or country, the name must be at least 3 characters and lookups share a smaller global capacity, so a 429 can mean the service is busy rather than that your key is over its limit.

Provide city and country whenever possible — they disambiguate venues with similar names and greatly improve match accuracy. If no confident match is found, the API returns a 404 with a reason (rather than guessing).

3. How to sign a request

For every request, build a "string to sign", compute an HMAC-SHA256 signature of it using your secret, and send three headers. The string to sign is five lines joined by newline characters:

METHOD            e.g. GET
PATH              e.g. /api/v1/public/venue-score   (path only, NO query string)
TIMESTAMP         current unix time in milliseconds
SHA256(query)     hex SHA-256 of the canonical query string (see below); "" if there is none
SHA256(body)      hex SHA-256 of the request body; for GET the body is "" (empty)

So the string to sign is literally:

stringToSign = METHOD + "\n" + PATH + "\n" + TIMESTAMP + "\n" + sha256_hex(canonicalQuery) + "\n" + sha256_hex(body)

Canonical query: take every name=value pair, percent-decode it, re-encode name and value with RFC 3986 rules (only A-Z a-z 0-9 - _ . ~ stay unescaped; spaces become %20; use uppercase hex), sort the pairs by name and then value, and join them with "&". For example name=O2 Academy&city=Brixton becomes city=Brixton&name=O2%20Academy. The query is therefore signed, so it cannot be changed after you sign.

Then compute the signature and send these headers:

X-SR-Key:        <your Key ID>
X-SR-Timestamp:  <the same TIMESTAMP you signed>
X-SR-Signature:  hex( HMAC_SHA256(secret, stringToSign) )

Important details: the PATH is signed without the query string, and the query is signed separately in canonical form. The timestamp must be within 5 minutes of our server time. Each signature can be used once: sign every request, including every retry, with a fresh timestamp. The signature is lowercase hex.

4. Worked example (bash)

KEY_ID="srk_live_xxxxxxxx"
SECRET="srs_live_xxxxxxxx"
TS=$(date +%s000)                              # unix ms
METHOD="GET"
PATH_ONLY="/api/v1/public/venue-score"
QUERY="city=Brixton&country=United%20Kingdom&name=O2%20Academy"   # canonical: sorted, RFC 3986 encoded
QUERY_HASH=$(printf '%s' "$QUERY" | openssl dgst -sha256 -hex | sed 's/^.* //')
BODY_HASH=$(printf '' | openssl dgst -sha256 -hex | sed 's/^.* //')
STR=$(printf '%s\n%s\n%s\n%s\n%s' "$METHOD" "$PATH_ONLY" "$TS" "$QUERY_HASH" "$BODY_HASH")
SIG=$(printf '%s' "$STR" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //')

curl -s "https://stagerage.online$PATH_ONLY?$QUERY" \
  -H "X-SR-Key: $KEY_ID" \
  -H "X-SR-Timestamp: $TS" \
  -H "X-SR-Signature: $SIG"

5. Worked example (Node.js)

const crypto = require("crypto");
const KEY_ID = "srk_live_xxxxxxxx";
const SECRET = "srs_live_xxxxxxxx";

const enc = v => encodeURIComponent(v).replace(/[!'()*]/g, c => "%" + c.charCodeAt(0).toString(16).toUpperCase());
const sha256 = v => crypto.createHash("sha256").update(v).digest("hex");

// params: plain object. Returns { query, headers } - send exactly this query.
function signedRequest(method, path, params, body = "") {
  const ts = Date.now().toString();
  const query = Object.entries(params).map(([k, v]) => [enc(k), enc(v)])
    .sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0))
    .map(([k, v]) => k + "=" + v).join("&");
  const stringToSign = [method, path, ts, sha256(query), sha256(body)].join("\n");
  const sig = crypto.createHmac("sha256", SECRET).update(stringToSign).digest("hex");
  return { query, headers: { "X-SR-Key": KEY_ID, "X-SR-Timestamp": ts, "X-SR-Signature": sig } };
}

const path = "/api/v1/public/venue-score";
const { query, headers } = signedRequest("GET", path, { name: "O2 Academy", city: "Brixton", country: "United Kingdom" });
fetch("https://stagerage.online" + path + "?" + query, { headers })
  .then(r => r.json())
  .then(console.log);

6. Response

On a confident match (200):

{
  "matched": true,
  "confidence": 0.92,
  "venue": { "name": "O2 Academy Brixton", "city": "Brixton", "country": "United Kingdom" },
  "scores": {
    "overall": 4.3, "staff": 4.5, "sound": 4.1, "lighting": 4.0,
    "stage": 4.2, "loadin_area": 3.8, "loadin_access": 4.0,
    "bus_power": 3.5, "dressing_room": 4.4
  },
  "_note": "Aggregate scores only. Scores are 0 where no reviews exist for that topic."
}

Scores are on a 0–5 scale. A score of 0 means no reviews exist for that topic yet (not a rating of zero). When no confident match is found, you get a 404 with a reason such as "below_confidence_threshold" or "ambiguous_match", plus a best_guess to help you refine the query.

7. Errors & limits

401 = signature/auth failure (check your string-to-sign, timestamp freshness, and that PATH excludes the query string). 429 = rate limit exceeded — back off and retry after the Retry-After header. 400 = missing required "name". 404 = no confident venue match. Each key has a per-minute rate limit shown in the X-RateLimit-Limit response header.

8. Query parameter formats

Send parameters as URL-encoded query string values:

name (required) — the venue name. Matching is fuzzy and tolerant of punctuation, accents, and common filler words ("The", "Club", "Hall", etc.), so "O2 Academy" matches "The O2 Academy". Accuracy improves a lot when you also send city and country.

city (recommended) — the city name. Matched case-insensitively but otherwise exactly, so spelling should match the real city (e.g. "Ljubljana", "New York").

country (recommended) — the country. We normalize common variants automatically, so you can send "UK", "U.K.", "GB", "Great Britain", or "United Kingdom" and they all resolve to the same country; likewise "US", "USA", "United States". When in doubt, the full English country name is always accepted (e.g. "Slovenia", "Germany", "United States", "United Kingdom").

If you send a city/country that matches no venues, you get a 404 with reason "no_venues_in_scope" — that means the scope filter excluded everything before name matching even ran. Double-check the city/country spelling.

9. Troubleshooting

401 "Signature verification failed" — your computed signature didn't match ours. The usual causes, in order of likelihood: (a) the PATH you signed includes the query string — it must NOT; sign only "/api/v1/public/venue-score" and put the canonical query in its own hash line. This is also the error you get from clients still using the old four-line format that left the query out. (b) You signed a different timestamp than the one you sent in X-SR-Timestamp — they must be identical. (c) Your string-to-sign uses real newlines; make sure you join with the newline character, not the literal text backslash-n. (d) The body hash is wrong; for GET it must be the SHA-256 of an empty string. (e) The query hash does not match the canonical query (sorted, RFC 3986 encoded).

401 "Signature already used" — each signature is accepted once. Sign again with a fresh timestamp, including when retrying a failed request.

401 "Request timestamp outside the allowed window" — your clock is more than 5 minutes off from ours, or you reused an old timestamp. Generate a fresh timestamp per request and keep your server clock synced (NTP).

401 "Invalid or revoked API key" — the X-SR-Key is wrong, or the key was revoked/rotated. If you rotated, update both the key_id and the secret.

429 "Rate limit exceeded" — back off and retry after the Retry-After header (seconds). The X-RateLimit-Limit and X-RateLimit-Remaining headers tell you your budget.

404 "matched: false" — not an error. Either no venue in scope (check city/country), or no confident name match. The response includes a best_guess to help you refine.

10. Quick checklist

Before going live: (1) confirm a signed GET returns 200 for a venue you know exists; (2) confirm your timestamp is in milliseconds and fresh per request; (3) confirm you sign the path without the query string and the canonical query hash as the fourth line; (4) handle 404 (no match) and 429 (rate limit) gracefully; (5) store your secret securely and never send it in a request — it's only used to compute the signature.

Questions or a key request? Contact StageRage support.