curl --request POST \
--url https://sailbox-api.sailresearch.com/v1/sailboxes/{sailbox_id}/listeners \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"guest_port": 8000,
"protocol": "http",
"allowlist": [
"203.0.113.0/24"
]
}
'import requests
url = "https://sailbox-api.sailresearch.com/v1/sailboxes/{sailbox_id}/listeners"
payload = {
"guest_port": 8000,
"protocol": "http",
"allowlist": ["203.0.113.0/24"]
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({guest_port: 8000, protocol: 'http', allowlist: ['203.0.113.0/24']})
};
fetch('https://sailbox-api.sailresearch.com/v1/sailboxes/{sailbox_id}/listeners', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://sailbox-api.sailresearch.com/v1/sailboxes/{sailbox_id}/listeners",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'guest_port' => 8000,
'protocol' => 'http',
'allowlist' => [
'203.0.113.0/24'
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://sailbox-api.sailresearch.com/v1/sailboxes/{sailbox_id}/listeners"
payload := strings.NewReader("{\n \"guest_port\": 8000,\n \"protocol\": \"http\",\n \"allowlist\": [\n \"203.0.113.0/24\"\n ]\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://sailbox-api.sailresearch.com/v1/sailboxes/{sailbox_id}/listeners")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"guest_port\": 8000,\n \"protocol\": \"http\",\n \"allowlist\": [\n \"203.0.113.0/24\"\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://sailbox-api.sailresearch.com/v1/sailboxes/{sailbox_id}/listeners")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"guest_port\": 8000,\n \"protocol\": \"http\",\n \"allowlist\": [\n \"203.0.113.0/24\"\n ]\n}"
response = http.request(request)
puts response.read_body{
"guest_port": 8000,
"protocol": "http",
"public_url": "https://sb-9c8f1e2a-3b4d-4f5a-8c7e-1d2f3a4b5c6d-8000.sail.box"
}{
"error": {
"message": "memory_limit_gib must be between 8 and 256 for 4 vCPUs",
"type": "invalid_request_error",
"param": null,
"code": null
}
}{
"error": {
"message": "Invalid API key",
"type": "authentication_error",
"param": null,
"code": "invalid_api_key"
}
}{
"error": {
"message": "Your API key has been disabled due to insufficient credits. Visit https://app.sailresearch.com/billing to add credits.",
"type": "billing_error",
"param": null,
"code": "credits_exhausted",
"billing_url": "https://app.sailresearch.com/billing"
}
}{
"error": {
"message": "sailboxes require an organization-scoped API key",
"type": "permission_error",
"param": null,
"code": null
}
}{
"error": {
"message": "sailbox \"sb_9c8f1e2a-3b4d-4f5a-8c7e-1d2f3a4b5c6d\" not found",
"type": "not_found_error",
"param": null,
"code": null
}
}{
"error": {
"message": "idempotency key reused with a different request body",
"type": "conflict_error",
"param": null,
"code": null
}
}{
"error": {
"message": "request body too large",
"type": "invalid_request_error",
"param": null,
"code": null
}
}{
"error": {
"message": "Too many concurrent requests. Please retry after some of your organization's in-flight requests complete.",
"type": "rate_limit_error",
"param": null,
"code": "rate_limited"
}
}{
"error": {
"message": "failed to fetch sailbox",
"type": "server_error",
"param": null,
"code": null
}
}{
"error": {
"message": "Authentication service unavailable",
"type": "server_error",
"param": null,
"code": null
}
}{
"error": {
"message": "idempotent request still in flight",
"type": "server_error",
"param": null,
"code": null
}
}Publish a port
Publishes a port from inside the Sailbox so it can be reached from outside. This works on a running Sailbox without restarting it, and on a paused or sleeping one, except for the udp cases in UDP ports.
An http listener gets an HTTPS URL. A tcp listener gets a host and port to dial, which is what SSH and database clients need. A udp listener gets a host and port for datagrams. A guest port can have a tcp and a udp listener, which share one host and port.
Without an allowlist the port is reachable by anyone who has the address, so set one unless you mean to publish to the internet.
Publishing a port that is already published under the same protocol sets that listener’s allowlist to what you send, so send the whole list every time. Sending none clears the restriction and reopens the port.
curl --request POST \
--url https://sailbox-api.sailresearch.com/v1/sailboxes/{sailbox_id}/listeners \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"guest_port": 8000,
"protocol": "http",
"allowlist": [
"203.0.113.0/24"
]
}
'import requests
url = "https://sailbox-api.sailresearch.com/v1/sailboxes/{sailbox_id}/listeners"
payload = {
"guest_port": 8000,
"protocol": "http",
"allowlist": ["203.0.113.0/24"]
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({guest_port: 8000, protocol: 'http', allowlist: ['203.0.113.0/24']})
};
fetch('https://sailbox-api.sailresearch.com/v1/sailboxes/{sailbox_id}/listeners', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://sailbox-api.sailresearch.com/v1/sailboxes/{sailbox_id}/listeners",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'guest_port' => 8000,
'protocol' => 'http',
'allowlist' => [
'203.0.113.0/24'
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://sailbox-api.sailresearch.com/v1/sailboxes/{sailbox_id}/listeners"
payload := strings.NewReader("{\n \"guest_port\": 8000,\n \"protocol\": \"http\",\n \"allowlist\": [\n \"203.0.113.0/24\"\n ]\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://sailbox-api.sailresearch.com/v1/sailboxes/{sailbox_id}/listeners")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"guest_port\": 8000,\n \"protocol\": \"http\",\n \"allowlist\": [\n \"203.0.113.0/24\"\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://sailbox-api.sailresearch.com/v1/sailboxes/{sailbox_id}/listeners")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"guest_port\": 8000,\n \"protocol\": \"http\",\n \"allowlist\": [\n \"203.0.113.0/24\"\n ]\n}"
response = http.request(request)
puts response.read_body{
"guest_port": 8000,
"protocol": "http",
"public_url": "https://sb-9c8f1e2a-3b4d-4f5a-8c7e-1d2f3a4b5c6d-8000.sail.box"
}{
"error": {
"message": "memory_limit_gib must be between 8 and 256 for 4 vCPUs",
"type": "invalid_request_error",
"param": null,
"code": null
}
}{
"error": {
"message": "Invalid API key",
"type": "authentication_error",
"param": null,
"code": "invalid_api_key"
}
}{
"error": {
"message": "Your API key has been disabled due to insufficient credits. Visit https://app.sailresearch.com/billing to add credits.",
"type": "billing_error",
"param": null,
"code": "credits_exhausted",
"billing_url": "https://app.sailresearch.com/billing"
}
}{
"error": {
"message": "sailboxes require an organization-scoped API key",
"type": "permission_error",
"param": null,
"code": null
}
}{
"error": {
"message": "sailbox \"sb_9c8f1e2a-3b4d-4f5a-8c7e-1d2f3a4b5c6d\" not found",
"type": "not_found_error",
"param": null,
"code": null
}
}{
"error": {
"message": "idempotency key reused with a different request body",
"type": "conflict_error",
"param": null,
"code": null
}
}{
"error": {
"message": "request body too large",
"type": "invalid_request_error",
"param": null,
"code": null
}
}{
"error": {
"message": "Too many concurrent requests. Please retry after some of your organization's in-flight requests complete.",
"type": "rate_limit_error",
"param": null,
"code": "rate_limited"
}
}{
"error": {
"message": "failed to fetch sailbox",
"type": "server_error",
"param": null,
"code": null
}
}{
"error": {
"message": "Authentication service unavailable",
"type": "server_error",
"param": null,
"code": null
}
}{
"error": {
"message": "idempotent request still in flight",
"type": "server_error",
"param": null,
"code": null
}
}Authorizations
Bearer authentication header of the form Bearer <token>, where <token> is your auth token.
Headers
Makes the request retry-safe. Sail remembers the answer it sent under a key, including a 400 or a 409, and replays it with Idempotent-Replayed: true for a retry that repeats the key, the method, the path, and the body. Bodies are compared byte for byte. Reusing a key for a different request returns 409, so send a corrected request under a new key. A key that is blank or only spaces is ignored, and the request runs without idempotency.
Any string of up to 255 bytes works, and characters outside ASCII count for more than one. A UUID is a good default. Keys are remembered for at least 24 hours and scoped to the API key that sent them.
A server error, or a failure to record the answer, can leave a key unsettled, and creating a Sailbox is where that matters. See Retrying safely.
255Path Parameters
Id of the Sailbox, as returned by create. It is sb_ followed by a UUID.
Body
Port your process listens on inside the Sailbox. Ports 10000, 10001, 15001, and 15002 are reserved and cannot be published on any protocol. Port 22 is reserved on http and available on tcp, which is what SSH needs.
A guest port can have a tcp listener and a udp listener at once. They share one public host and port, and each has its own allowlist. Publishing a port again under the same protocol replaces that listener's allowlist. Publishing http on a port with a tcp or udp listener, or the reverse, is refused.
1 <= x <= 65535Restricts who can reach the listener. An address or a range, such as 203.0.113.0/24, lets matching source IPs in, and works on every protocol. An app name, which lets Sailboxes in that app call this one, works on http only and is rejected on tcp and udp.
An app name must not look like an address or a range, and must not contain a /. Every entry that reads as an address or a range is taken as one, and any other entry holding a / is refused as a malformed range. An address must not carry an IPv6 zone, such as fe80::1%eth0, which names an interface on one machine rather than a source.
Entries come back canonicalized: a range is reported by its network address, a bare address as a single-address range, and blank or repeated entries are dropped.
Leaving it empty makes the listener reachable by anyone who has the address. That is refused on a tcp listener for a well-known unauthenticated service port such as 5432 or 6379, and on a udp listener for a well-known amplification port such as 53 or 123: publish those with an explicit allowlist, using 0.0.0.0/0 and ::/0 if you do want the whole internet.
Publishes the port only if the Sailbox still has this visibility, and otherwise fails with 409. Set it when what you set up inside the Sailbox depends on its visibility, so a change made while you set up cannot publish the port for the wrong audience. SSH setup sends it.
org, private Response
The port is published.
Port inside the Sailbox that traffic is sent to.
Protocol the listener serves. Values in use today are http, tcp, and udp. New values can appear over time, so treat this as an open set.
HTTPS URL for an http listener.
Host to reach a tcp or udp listener at.
Port to reach a tcp or udp listener at.
Sources allowed to reach the listener: addresses, ranges, and app names.