How dates and times are handled
The Sally API uses one format everywhere, in both directions: ISO-8601 in UTC with a trailing Z. This page lists the rules, the parameters that filter by date and the one trap to avoid when you filter by day.
The examples show how to turn a day in your time zone into exact UTC bounds and how to convert timestamps from a response back for display. They work as they stand once SALLY_TOKEN and DIRECTORY_ID are set, see the Quickstart.
Quick navigation
1. The format
Every timestamp has the form YYYY-MM-DDTHH:MM:SSZ:
2026-08-27T14:30:00Z
That applies to every date-time field in a response, such as createdOn or startTime, and to every date input in a query string or body.
followUpMeetingStartTime and followUpMeetingEndTime on a summary section item with sectionType: appointment are a local date and time without a time zone, for example 2026-10-16T09:00:00. What the meeting agreed on is a wall-clock hour, Thursday at nine, and a zone would move that hour for every reader in a different one. Apply the time zone of the people involved instead of reading the value as UTC. Get meeting summaries describes these fields.
2. The rules
- Timestamps in responses are always in UTC. The only exception are the two follow-up fields above.
- Inputs must be UTC. A value without a time zone suffix is read as UTC, never as server time.
- An explicit offset is converted to the matching UTC instant, so
2026-08-27T16:30:00+02:00is the same instant as the example above. - A date without a time (
2026-08-27) counts as midnight UTC.
| You send | The API reads it as |
|---|---|
2026-08-27T14:30:00Z | 2026-08-27T14:30:00Z |
2026-08-27T16:30:00+02:00 | 2026-08-27T14:30:00Z |
2026-08-27T14:30:00 | 2026-08-27T14:30:00Z, no suffix means UTC |
2026-08-27 | 2026-08-27T00:00:00Z, midnight UTC |
Always send the full form with Z. Then your request says exactly what the API reads, and nobody has to remember the rules above.
3. Date filters
These parameters filter by date. Each bound is inclusive:
| Endpoint | Parameter | Filters on | Condition |
|---|---|---|---|
| Appointment list | from (query) | start time | at or after |
| Appointment list | to (query) | start time | at or before |
| Appointment search | from (body) | start time | at or after |
| Appointment search | to (body) | start time | at or before |
| Recording list | createdAfter (query) | creation time | at or after |
| Recording list | createdBefore (query) | creation time | at or before |
This request lists the recordings created in September 2026 (UTC):
- cURL
- JavaScript
- Python
curl "https://api.sally.io/v1.0/directories/$DIRECTORY_ID/recordings?createdAfter=2026-09-01T00:00:00Z&createdBefore=2026-09-30T23:59:59Z" \
-H "Authorization: Bearer $SALLY_TOKEN"
const url = new URL(`https://api.sally.io/v1.0/directories/${directoryId}/recordings`);
url.searchParams.set('createdAfter', '2026-09-01T00:00:00Z');
url.searchParams.set('createdBefore', '2026-09-30T23:59:59Z');
const response = await fetch(url, {
headers: {Authorization: `Bearer ${process.env.SALLY_TOKEN}`},
});
const {items} = await response.json();
import os
import requests
response = requests.get(
f"https://api.sally.io/v1.0/directories/{directory_id}/recordings",
params={
"createdAfter": "2026-09-01T00:00:00Z",
"createdBefore": "2026-09-30T23:59:59Z",
},
headers={"Authorization": f"Bearer {os.environ['SALLY_TOKEN']}"},
)
items = response.json()["items"]
- GET
…/appointmentsSearches appointments (subject, attendee, time range), paginatedTry it - POST
…/appointments/searchSearches appointments (parameters in the body)Try it - GET
…/recordingsLists recordings of a directory (paginated, optionally filtered by date)Try it
4. Filtering by day
In summer a day in Berlin starts two hours before midnight UTC, so from=2026-08-27 is not the German 27th. Convert on your side and send the exact instant you mean, for example 2026-08-26T22:00:00Z.
| 27 August 2026 in Berlin (UTC+2) | Local time | UTC to send |
|---|---|---|
| Start of day | 2026-08-27 00:00:00 | 2026-08-26T22:00:00Z |
| End of day | 2026-08-27 23:59:59 | 2026-08-27T21:59:59Z |
What from=2026-08-27 means | 2026-08-27 02:00:00 | 2026-08-27T00:00:00Z |
This code computes both bounds for a day in any time zone and lists the appointments that start on that day:
- cURL
- JavaScript
- Python
# 27 August 2026 in Berlin (UTC+2) as exact UTC bounds
from="2026-08-26T22:00:00Z"
to="2026-08-27T21:59:59Z"
curl "https://api.sally.io/v1.0/directories/$DIRECTORY_ID/appointments?from=$from&to=$to" \
-H "Authorization: Bearer $SALLY_TOKEN"
// Turns a local wall-clock time in a time zone into a UTC timestamp.
function toUtc(localDateTime, timeZone) {
const guess = new Date(`${localDateTime}Z`);
const inZone = new Date(guess.toLocaleString('en-US', {timeZone}));
const inUtc = new Date(guess.toLocaleString('en-US', {timeZone: 'UTC'}));
const utc = new Date(guess.getTime() - (inZone - inUtc));
return utc.toISOString().replace('.000Z', 'Z');
}
const from = toUtc('2026-08-27T00:00:00', 'Europe/Berlin'); // 2026-08-26T22:00:00Z
const to = toUtc('2026-08-27T23:59:59', 'Europe/Berlin'); // 2026-08-27T21:59:59Z
const url = new URL(`https://api.sally.io/v1.0/directories/${directoryId}/appointments`);
url.searchParams.set('from', from);
url.searchParams.set('to', to);
const response = await fetch(url, {
headers: {Authorization: `Bearer ${process.env.SALLY_TOKEN}`},
});
const {items} = await response.json();
import os
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
import requests
berlin = ZoneInfo("Europe/Berlin")
def to_utc(local):
return local.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
start = to_utc(datetime(2026, 8, 27, 0, 0, 0, tzinfo=berlin)) # 2026-08-26T22:00:00Z
end = to_utc(datetime(2026, 8, 27, 23, 59, 59, tzinfo=berlin)) # 2026-08-27T21:59:59Z
response = requests.get(
f"https://api.sally.io/v1.0/directories/{directory_id}/appointments",
params={"from": start, "to": end},
headers={"Authorization": f"Bearer {os.environ['SALLY_TOKEN']}"},
)
items = response.json()["items"]
Use a time zone name such as Europe/Berlin, not a fixed offset. The name follows daylight saving time, so the same code gives +01:00 in winter and +02:00 in summer.
5. Showing times to users
Every timestamp in a response is UTC:
{
"appointmentId": "d4398f97-cd92-4d21-a6ec-4aab0a85441b",
"subject": "Quarterly planning",
"startTime": "2026-08-27T07:30:00Z",
"endTime": "2026-08-27T08:30:00Z"
}
Convert it to the user's time zone only when you display it, and keep the UTC value for storing and comparing:
- cURL
- JavaScript
- Python
# GNU date: 2026-08-27T07:30:00Z shown in Berlin time
TZ=Europe/Berlin date -d '2026-08-27T07:30:00Z' '+%Y-%m-%d %H:%M'
# 2026-08-27 09:30
const startTime = new Date('2026-08-27T07:30:00Z');
console.log(startTime.toLocaleString('en-GB', {timeZone: 'Europe/Berlin'}));
// 27/08/2026, 09:30:00
from datetime import datetime
from zoneinfo import ZoneInfo
start_time = datetime.fromisoformat("2026-08-27T07:30:00Z".replace("Z", "+00:00"))
print(start_time.astimezone(ZoneInfo("Europe/Berlin")).strftime("%Y-%m-%d %H:%M"))
# 2026-08-27 09:30