curl --request POST \
--url https://sailbox-api.sailresearch.com/v1/sailboxes/{sailbox_id}/files/download-url \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"path": "/workspace/render.png",
"expires_in_seconds": 3600
}
'import requests
url = "https://sailbox-api.sailresearch.com/v1/sailboxes/{sailbox_id}/files/download-url"
payload = {
"path": "/workspace/render.png",
"expires_in_seconds": 3600
}
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({path: '/workspace/render.png', expires_in_seconds: 3600})
};
fetch('https://sailbox-api.sailresearch.com/v1/sailboxes/{sailbox_id}/files/download-url', 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}/files/download-url",
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([
'path' => '/workspace/render.png',
'expires_in_seconds' => 3600
]),
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}/files/download-url"
payload := strings.NewReader("{\n \"path\": \"/workspace/render.png\",\n \"expires_in_seconds\": 3600\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}/files/download-url")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"path\": \"/workspace/render.png\",\n \"expires_in_seconds\": 3600\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://sailbox-api.sailresearch.com/v1/sailboxes/{sailbox_id}/files/download-url")
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 \"path\": \"/workspace/render.png\",\n \"expires_in_seconds\": 3600\n}"
response = http.request(request)
puts response.read_body{
"url": "https://sailbox-api.sailresearch.com/v1/sailboxes/sb_9c8f1e2a-3b4d-4f5a-8c7e-1d2f3a4b5c6d/files/download?expires=1767225600&path=%2Fworkspace%2Frender.png&sig=Yx3n4x1p9Q8b2Vf7K0mZcJtR6wLaHd5eGsUoNiA8PkE",
"expires_at": "2026-01-01T00:00:00Z"
}{
"error": {
"message": "memory_limit_gib for size m must be between 8 and 128",
"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": "sailbox listeners require persistence",
"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
}
}Mint a download URL for a file
Returns a URL that serves one file over HTTPS with no API key. The URL authorizes reading that file alone and serves whatever the file holds when it is fetched. Share a file by URL covers the response headers, caching, and sleeping or paused Sailboxes.
curl --request POST \
--url https://sailbox-api.sailresearch.com/v1/sailboxes/{sailbox_id}/files/download-url \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"path": "/workspace/render.png",
"expires_in_seconds": 3600
}
'import requests
url = "https://sailbox-api.sailresearch.com/v1/sailboxes/{sailbox_id}/files/download-url"
payload = {
"path": "/workspace/render.png",
"expires_in_seconds": 3600
}
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({path: '/workspace/render.png', expires_in_seconds: 3600})
};
fetch('https://sailbox-api.sailresearch.com/v1/sailboxes/{sailbox_id}/files/download-url', 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}/files/download-url",
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([
'path' => '/workspace/render.png',
'expires_in_seconds' => 3600
]),
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}/files/download-url"
payload := strings.NewReader("{\n \"path\": \"/workspace/render.png\",\n \"expires_in_seconds\": 3600\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}/files/download-url")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"path\": \"/workspace/render.png\",\n \"expires_in_seconds\": 3600\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://sailbox-api.sailresearch.com/v1/sailboxes/{sailbox_id}/files/download-url")
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 \"path\": \"/workspace/render.png\",\n \"expires_in_seconds\": 3600\n}"
response = http.request(request)
puts response.read_body{
"url": "https://sailbox-api.sailresearch.com/v1/sailboxes/sb_9c8f1e2a-3b4d-4f5a-8c7e-1d2f3a4b5c6d/files/download?expires=1767225600&path=%2Fworkspace%2Frender.png&sig=Yx3n4x1p9Q8b2Vf7K0mZcJtR6wLaHd5eGsUoNiA8PkE",
"expires_at": "2026-01-01T00:00:00Z"
}{
"error": {
"message": "memory_limit_gib for size m must be between 8 and 128",
"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": "sailbox listeners require persistence",
"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
}
}Authorizations
Bearer authentication header of the form Bearer <token>, where <token> is your auth token.
Path Parameters
Id of the Sailbox, as returned by create. It is sb_ followed by a UUID.
Body
Absolute path of the file inside the Sailbox. The file is read when the URL is fetched, so it does not have to exist when the URL is minted.
1How long the URL stays valid, up to seven days. Expiry only stops new fetches; copies already downloaded or cached are unaffected.
1 <= x <= 604800Serve the file as a download (Content-Disposition: attachment) instead of inline.
Mark the response cacheable for a year, so a cache that holds a copy never checks back. Use it only for a path you never overwrite.