Update a status page
const url = 'https://dev.tunnelhq.com/api/v1/status-pages/sp_12';const options = { method: 'PATCH', headers: { 'X-Project-Id': 'prj_12', 'X-API-Key': '<X-API-Key>', 'Content-Type': 'application/json' }, body: '{"website_url":"https://larkspur.example","brand_color":"#3b6fd4","theme":"auto"}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request PATCH \ --url https://dev.tunnelhq.com/api/v1/status-pages/sp_12 \ --header 'Content-Type: application/json' \ --header 'X-API-Key: <X-API-Key>' \ --header 'X-Project-Id: prj_12' \ --data '{ "website_url": "https://larkspur.example", "brand_color": "#3b6fd4", "theme": "auto" }'Changes the fields you send; the rest stay as they are. An unknown field is refused. It needs a role that can edit the project (Owner, Admin and Manager have it by default).
On a published page, changes to what it shows wait for a
publish (changes_pending).
search_visible, auto_incidents, monitoring_operational, templates and password take effect at once.
A new slug moves the page: the old address stops answering.
Pictures (logo, logo_dark, favicon, share_image) are sent as data URIs, and null removes one. Or send a link
instead (logo_url, logo_dark_url, favicon_url, share_image_url): the server fetches the picture and holds it
to the same types and sizes. A link to a private address, or to one of TunnelHQ’s own, is not fetched. Send a picture
or its link, not both. To take them from the page’s website, use
Get the logo and icons from the website.
A request body can be at most 1 MB.
logo_with_name is whether the page’s name shows beside the logo. null leaves it to the logo’s shape: a square mark
gets the name, a wide logo that spells it does not. A new logo hands it back to the logo’s shape, unless the same
call sends logo_with_name.
Need an API key? See Getting an API key.
Authorizations
Section titled “Authorizations”Parameters
Section titled “ Parameters ”Path Parameters
Section titled “Path Parameters”Header Parameters
Section titled “Header Parameters”The project, as prj_12 or 12. A workspace key can only reach its own workspace’s projects.
Example
prj_12Request Bodyrequired
Section titled “Request Bodyrequired”object
A new address. The old one stops answering.
Your website, linked from the logo. http:// or https://.
Get in touch: an https:// page or a mailto: address.
A line under the headline; also the text of search results and shared links.
The browser tab’s title. Empty is <name> status.
Six hex digits as #rrggbb, for buttons and links. null for the default.
auto follows each visitor’s device.
true or false to choose; null leaves it to the logo’s shape.
false asks search engines not to list the page. It is not privacy.
Open an incident by itself when a service on the page goes down
While an incident is Monitoring, its services show what their checks show.
The text a new update starts with, for each status. Sending it replaces the whole set; a status left out is empty.
object
A data URI: PNG, JPEG, WebP or SVG, 300 KB at most.
For the dark theme. A data URI: PNG, JPEG, WebP or SVG, 300 KB at most.
A data URI: PNG, SVG, ICO, GIF or JPEG, 50 KB at most.
A data URI: PNG or JPEG, 500 KB at most, best at 1200 by 630.
A link to the logo; the server fetches it. Instead of logo.
A link to the logo for the dark theme. Instead of logo_dark.
A link to the favicon. Instead of favicon.
A link to the share image. Instead of share_image.
8 to 72 bytes makes the page private. null or "" makes it public again.
Examples
Colours and website
{ "website_url": "https://larkspur.example", "brand_color": "#3b6fd4", "theme": "auto"}A logo by link
{ "logo_url": "https://larkspur.example/logo.svg", "favicon_url": "https://larkspur.example/favicon.png"}Responses
Section titled “ Responses ”The updated page.
object
The page’s own address, without https://.
Where visitors find the page. It uses the custom domain only when the page is published and domain_state is active.
A published page has changes that wait for a publish.
none: no custom domain is set.waiting: set, but its record doesn’t point to TunnelHQ yet.domain_notesays what the check found.issuing: the record points here and the HTTPS certificate is being made.active: live, with HTTPS.failed: the certificate failed.domain_notesays why.
What the last check found
The record the custom domain needs, one CNAME to cname.tunnelhq.net; empty without a custom domain. For a bare
domain it’s named @: add it as your DNS provider’s CNAME flattening, ALIAS or ANAME record.
The record a custom domain needs, named as DNS providers name it: relative to your zone, status for status.example.com and @ for example.com itself.
object
Whether the page is behind a password.
Lowercase #rrggbb, or null.
logo: the logo and Subscribe. site: with Status, History and Maintenance tabs too.
bars: 90 days of bars under each service. folded: one line each, bars on a click.
Whether the name shows beside the logo now: your choice, or until you make one, the logo’s shape.
Other languages visitors can choose.
The text a new update starts with, for each status. Sending it replaces the whole set; a status left out is empty.
object
Services as visitors see them, hidden ones included: a server’s tunnels on one host count once, a group counts its members.
object
The section’s heading; empty for none.
object
The name shown on the page.
On the page’s setup, but not shown to visitors.
The monitor’s own name.
The monitor’s type, such as vpn, http or group.
A tunnel’s protocol, such as wireguard; null for other monitors.
A group’s members, one level deep. Empty for anything else.
object
Examples
A draft status page
{ "id": "sp_12", "name": "Larkspur VPN", "slug": "larkspur-vpn", "address": "larkspur-vpn.tunnelhq.net", "url": "https://larkspur-vpn.tunnelhq.net", "published": false, "published_at": null, "changes_pending": false, "custom_domain": "status.larkspur.example", "domain_state": "waiting", "domain_note": null, "dns_records": [ { "type": "CNAME", "name": "status", "value": "cname.tunnelhq.net" } ], "private": false, "website_url": null, "contact_url": null, "description": null, "page_title": null, "brand_color": null, "theme": "auto", "header": "site", "layout": "bars", "show_past_incidents": true, "logo_height": 32, "logo_with_name": false, "language": "en", "languages": [], "search_visible": true, "auto_incidents": true, "monitoring_operational": false, "templates": { "investigating": "", "identified": "", "monitoring": "", "resolved": "" }, "has_logo": false, "has_dark_logo": false, "has_favicon": false, "has_share_image": false, "service_count": 2, "service_limit": 400, "sections": [ { "id": 3, "name": "", "services": [ { "id": "sps_31", "monitor_id": "srv_40", "name": "Europe", "hidden": false, "monitor_name": "Europe", "type": "group", "protocol": null, "services": [ { "id": "sps_32", "monitor_id": "srv_42", "name": "Frankfurt 1", "hidden": false, "monitor_name": "Frankfurt 1 WireGuard", "type": "vpn", "protocol": "wireguard", "services": [] }, { "id": "sps_33", "monitor_id": "srv_43", "name": "Frankfurt 1", "hidden": false, "monitor_name": "Frankfurt 1 OpenVPN", "type": "vpn", "protocol": "openvpn", "services": [] } ] }, { "id": "sps_34", "monitor_id": "srv_80", "name": "larkspur.example", "hidden": false, "monitor_name": "larkspur.example", "type": "http", "protocol": null, "services": [] } ] } ]}Headers
Section titled “Headers”Requests the workspace’s plan allows per minute.
Requests left in the current minute.
Unix time, in seconds, when the current minute ends.
A field is unknown or its value isn’t allowed.
object
The HTTP status, repeated.
What went wrong.
More detail, on some errors.
Seconds to wait, on the 429 for wrong keys.
Examples
{ "error": true, "code": 400, "message": "Unknown field: x. The fields are name, slug, website_url, contact_url, description, page_title, brand_color, theme, header, layout, show_past_incidents, logo_height, language, languages, search_visible, auto_incidents, monitoring_operational, templates, logo, logo_dark, favicon, share_image, password, logo_with_name, logo_url, logo_dark_url, favicon_url, share_image_url."}{ "error": true, "code": 400, "message": "show_past_incidents must be true or false."}{ "error": true, "code": 400, "message": "logo_height must be one of 24, 32, 40, 48."}{ "error": true, "code": 400, "message": "The colour must be six hex digits, such as #1d4ed8."}{ "error": true, "code": 400, "message": "A password must be at least 8 characters, and at most 72."}{ "error": true, "code": 400, "message": "The logo can be at most 300 KB."}{ "error": true, "code": 400, "message": "The website link must be a full address, such as https://example.com."}{ "error": true, "code": 400, "message": "Send logo_url or logo, not both."}{ "error": true, "code": 400, "message": "logo_url must be an http or https link to the picture."}{ "error": true, "code": 400, "message": "logo_url: larkspur.example did not give a PNG, JPEG, WebP or SVG of at most 300 KB."}{ "error": true, "code": 400, "message": "logo_url: 127.0.0.1 is a private address or one of TunnelHQ's own, so it is not fetched. Link a picture on a public website."}The API key is missing, wrong, expired, switched off, or no longer valid.
object
The HTTP status, repeated.
What went wrong.
More detail, on some errors.
Seconds to wait, on the 429 for wrong keys.
Examples
{ "error": true, "code": 401, "message": "API key is required. Provide X-API-Key header or Authorization: Bearer <key>"}{ "error": true, "code": 401, "message": "Invalid or expired API key"}{ "error": true, "code": 401, "message": "API key has expired"}{ "error": true, "code": 401, "message": "API key owner account is inactive"}{ "error": true, "code": 401, "message": "This API key's workspace access has been removed"}The role can’t edit the project.
object
The HTTP status, repeated.
What went wrong.
More detail, on some errors.
Seconds to wait, on the 429 for wrong keys.
Example
{ "error": true, "code": 403, "message": "Changing status pages needs a role that can edit this project."}The project has no status page with this ID.
object
The HTTP status, repeated.
What went wrong.
More detail, on some errors.
Seconds to wait, on the 429 for wrong keys.
Example
{ "error": true, "code": 404, "message": "No such status page."}The slug is taken by another status page.
object
The HTTP status, repeated.
What went wrong.
More detail, on some errors.
Seconds to wait, on the 429 for wrong keys.
Example
{ "error": true, "code": 409, "message": "That address is taken. Choose another."}A usage limit was reached, or this IP address made too many requests with a wrong key.
Usage limits are per workspace and plan: per minute (Retry-After: 60), per day
(Retry-After: 3600), and per month (no Retry-After). They use their own body shape.
An IP address may send 10 wrong keys a minute; one more is allowed every 6 seconds. While
that allowance is used up, every request from the address is refused, even one with a
valid key.
object
Seconds to wait. Absent for the monthly limit.
object
The HTTP status, repeated.
What went wrong.
More detail, on some errors.
Seconds to wait, on the 429 for wrong keys.
Examples
{ "error": "Rate limit exceeded", "message": "Per-minute limit of 60 requests exceeded", "retry_after": 60}{ "error": "Daily limit exceeded", "message": "Daily limit of 25000 requests exceeded", "retry_after": 3600}{ "error": "Monthly limit exceeded", "message": "Monthly limit of 250000 requests exceeded"}{ "error": true, "code": 429, "message": "Too many failed API key attempts from this address. Wait a few seconds and try again.", "retry_after": 6}Headers
Section titled “Headers”Seconds to wait. 60 for the per-minute limit, 3600 for the daily limit, 6 after wrong keys. Absent for the monthly limit.
Something failed on TunnelHQ’s side.
object
The HTTP status, repeated.
What went wrong.
More detail, on some errors.
Seconds to wait, on the 429 for wrong keys.
Example
{ "error": true, "code": 500, "message": "Internal server error"}