curl --request POST \
--url https://api.hellotracks.com/v1/eta/sessions \
--header 'API-Key: <api-key>' \
--header 'Content-Type: application/json' \
--data '
{
"jobId": "650000000000000000000001",
"recipientEmail": "sam@example.com",
"showMap": true,
"expiresIn": "7d"
}
'import requests
url = "https://api.hellotracks.com/v1/eta/sessions"
payload = {
"jobId": "650000000000000000000001",
"recipientEmail": "sam@example.com",
"showMap": True,
"expiresIn": "7d"
}
headers = {
"API-Key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'API-Key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
jobId: '650000000000000000000001',
recipientEmail: 'sam@example.com',
showMap: true,
expiresIn: '7d'
})
};
fetch('https://api.hellotracks.com/v1/eta/sessions', 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://api.hellotracks.com/v1/eta/sessions",
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([
'jobId' => '650000000000000000000001',
'recipientEmail' => 'sam@example.com',
'showMap' => true,
'expiresIn' => '7d'
]),
CURLOPT_HTTPHEADER => [
"API-Key: <api-key>",
"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://api.hellotracks.com/v1/eta/sessions"
payload := strings.NewReader("{\n \"jobId\": \"650000000000000000000001\",\n \"recipientEmail\": \"sam@example.com\",\n \"showMap\": true,\n \"expiresIn\": \"7d\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("API-Key", "<api-key>")
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://api.hellotracks.com/v1/eta/sessions")
.header("API-Key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"jobId\": \"650000000000000000000001\",\n \"recipientEmail\": \"sam@example.com\",\n \"showMap\": true,\n \"expiresIn\": \"7d\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.hellotracks.com/v1/eta/sessions")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["API-Key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"jobId\": \"650000000000000000000001\",\n \"recipientEmail\": \"sam@example.com\",\n \"showMap\": true,\n \"expiresIn\": \"7d\"\n}"
response = http.request(request)
puts response.read_body{
"data": {
"session": {
"id": "eta_k7m2p9x4q8r3",
"token": "k7m2p9x4q8r3",
"jobId": "650000000000000000000001",
"placeId": "p1q2r3",
"memberId": "a1b2c3",
"recipientName": "Sam Lee",
"recipientPhone": "+14155550102",
"recipientEmail": "sam@example.com",
"title": "Install water heater",
"status": "assigned",
"recipientStatus": "unknown",
"etaWindowStart": 1793552400000,
"etaWindowEnd": 1793563200000,
"etaConfidence": "scheduled",
"publicUrl": "https://eta.page/K7M2-P9X4-Q8R3",
"apiUrl": "https://api.hellotracks.com/api/eta/k7m2p9x4q8r3",
"showWorkerName": true,
"showMap": true,
"branding": {
"companyName": "Example Plumbing",
"companyId": "c1d2e3"
},
"viewedAt": 0,
"readyConfirmedAt": 0,
"accessNotes": "",
"rescheduleRequestedAt": 0,
"rescheduleNote": "",
"source": "public_api",
"createdById": "c1d2e3",
"createdByName": "Example Plumbing",
"createdAt": 1793520000000,
"updatedAt": 1793520000000,
"expiresAt": 1796112000000,
"revokedAt": 0,
"expired": false,
"revoked": false,
"job": {
"id": "650000000000000000000001",
"title": "Install water heater",
"date": "2026-11-02",
"status": "assigned",
"progressCode": 1,
"timeWindow": {
"start": "09:00",
"end": "12:00"
},
"address": "500 Market St, San Francisco, CA 94105"
},
"recipientSafe": {
"ready": false,
"accessNotesAdded": false,
"rescheduleRequested": false
}
}
},
"error": null,
"meta": {
"path": "/v1/eta/sessions",
"method": "POST"
}
}{
"data": null,
"error": {
"code": "invalid_request",
"message": "date must be YYYY-MM-DD"
},
"meta": {
"path": "/jobs",
"method": "GET"
}
}{
"data": null,
"error": {
"code": "invalid_api_key",
"message": "Invalid Hellotracks API key."
},
"meta": {
"path": "/jobs",
"method": "GET"
}
}{
"data": null,
"error": {
"code": "forbidden",
"message": "No permission."
},
"meta": {
"path": "/jobs",
"method": "GET"
}
}{
"data": null,
"error": {
"code": "throttled",
"message": "Too many requests."
},
"meta": {
"path": "/jobs",
"method": "GET"
}
}{
"data": null,
"error": {
"code": "internal_error",
"message": "Hellotracks API is temporarily unavailable."
},
"meta": {
"path": "/jobs",
"method": "GET"
}
}Create an ETA session
Creates a live ETA page (https://eta.page/…) for a job’s recipient. If the job already has an active session (not revoked, not expired), that one is returned as it is and the body is ignored. Recipient fields default to the job’s contact. Expiry defaults to 30 days, at most 90 days. Status 200, not 201. Workers are refused with 403 (code permission_denied); an unknown job gives 400.
curl --request POST \
--url https://api.hellotracks.com/v1/eta/sessions \
--header 'API-Key: <api-key>' \
--header 'Content-Type: application/json' \
--data '
{
"jobId": "650000000000000000000001",
"recipientEmail": "sam@example.com",
"showMap": true,
"expiresIn": "7d"
}
'import requests
url = "https://api.hellotracks.com/v1/eta/sessions"
payload = {
"jobId": "650000000000000000000001",
"recipientEmail": "sam@example.com",
"showMap": True,
"expiresIn": "7d"
}
headers = {
"API-Key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'API-Key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
jobId: '650000000000000000000001',
recipientEmail: 'sam@example.com',
showMap: true,
expiresIn: '7d'
})
};
fetch('https://api.hellotracks.com/v1/eta/sessions', 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://api.hellotracks.com/v1/eta/sessions",
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([
'jobId' => '650000000000000000000001',
'recipientEmail' => 'sam@example.com',
'showMap' => true,
'expiresIn' => '7d'
]),
CURLOPT_HTTPHEADER => [
"API-Key: <api-key>",
"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://api.hellotracks.com/v1/eta/sessions"
payload := strings.NewReader("{\n \"jobId\": \"650000000000000000000001\",\n \"recipientEmail\": \"sam@example.com\",\n \"showMap\": true,\n \"expiresIn\": \"7d\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("API-Key", "<api-key>")
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://api.hellotracks.com/v1/eta/sessions")
.header("API-Key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"jobId\": \"650000000000000000000001\",\n \"recipientEmail\": \"sam@example.com\",\n \"showMap\": true,\n \"expiresIn\": \"7d\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.hellotracks.com/v1/eta/sessions")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["API-Key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"jobId\": \"650000000000000000000001\",\n \"recipientEmail\": \"sam@example.com\",\n \"showMap\": true,\n \"expiresIn\": \"7d\"\n}"
response = http.request(request)
puts response.read_body{
"data": {
"session": {
"id": "eta_k7m2p9x4q8r3",
"token": "k7m2p9x4q8r3",
"jobId": "650000000000000000000001",
"placeId": "p1q2r3",
"memberId": "a1b2c3",
"recipientName": "Sam Lee",
"recipientPhone": "+14155550102",
"recipientEmail": "sam@example.com",
"title": "Install water heater",
"status": "assigned",
"recipientStatus": "unknown",
"etaWindowStart": 1793552400000,
"etaWindowEnd": 1793563200000,
"etaConfidence": "scheduled",
"publicUrl": "https://eta.page/K7M2-P9X4-Q8R3",
"apiUrl": "https://api.hellotracks.com/api/eta/k7m2p9x4q8r3",
"showWorkerName": true,
"showMap": true,
"branding": {
"companyName": "Example Plumbing",
"companyId": "c1d2e3"
},
"viewedAt": 0,
"readyConfirmedAt": 0,
"accessNotes": "",
"rescheduleRequestedAt": 0,
"rescheduleNote": "",
"source": "public_api",
"createdById": "c1d2e3",
"createdByName": "Example Plumbing",
"createdAt": 1793520000000,
"updatedAt": 1793520000000,
"expiresAt": 1796112000000,
"revokedAt": 0,
"expired": false,
"revoked": false,
"job": {
"id": "650000000000000000000001",
"title": "Install water heater",
"date": "2026-11-02",
"status": "assigned",
"progressCode": 1,
"timeWindow": {
"start": "09:00",
"end": "12:00"
},
"address": "500 Market St, San Francisco, CA 94105"
},
"recipientSafe": {
"ready": false,
"accessNotesAdded": false,
"rescheduleRequested": false
}
}
},
"error": null,
"meta": {
"path": "/v1/eta/sessions",
"method": "POST"
}
}{
"data": null,
"error": {
"code": "invalid_request",
"message": "date must be YYYY-MM-DD"
},
"meta": {
"path": "/jobs",
"method": "GET"
}
}{
"data": null,
"error": {
"code": "invalid_api_key",
"message": "Invalid Hellotracks API key."
},
"meta": {
"path": "/jobs",
"method": "GET"
}
}{
"data": null,
"error": {
"code": "forbidden",
"message": "No permission."
},
"meta": {
"path": "/jobs",
"method": "GET"
}
}{
"data": null,
"error": {
"code": "throttled",
"message": "Too many requests."
},
"meta": {
"path": "/jobs",
"method": "GET"
}
}{
"data": null,
"error": {
"code": "internal_error",
"message": "Hellotracks API is temporarily unavailable."
},
"meta": {
"path": "/jobs",
"method": "GET"
}
}Authorizations
Your API key, from More → API & Integrations in the dashboard, or from ht signup / ht auth login.
Body
Defaults to the job's contact name.
Defaults to the job's contact phone.
Defaults to the job's contact email.
Defaults to the job title, or 'Service visit'.
Defaults to the company setting etaPageDefaultShowWorkerName.
Defaults to the company setting etaPageDefaultShowMap.
How long the link lives: milliseconds, or a number with m, h or d (30m, 24h, 30d). Default 30 days, at most 90 days. 'expires' wins over 'expiresIn'.
Same as expiresIn.