Queues
A queue is a weekly pattern of publishing times. Instead of picking a timestamp for every post, you describe the rhythm once — "Monday and Wednesday at 09:30, Friday at 16:00" — and then hand posts to the queue. Each post takes the next free slot.
Queues belong to a profile and are resolved in that queue's timezone, so "09:30" survives daylight-saving changes without you recomputing anything.
Like posts, queues read with posts:read and write with posts:write. There is
no separate queue permission: a key that can schedule a post can shape the
schedule it goes into, and one that cannot, cannot.
Creating one
export async function createWeekdayQueue(
zhyper: ZhyperClient,
profileId: string,
): Promise<Schemas['QueueDto']> {
return zhyper.request<Schemas['QueueDto']>('createQueue', {
body: {
profileId,
name: 'Editorial',
// `dayOfWeek` 0 = Pazar ... 6 = Cumartesi. `time` yerel duvar saatidir
// ("HH:MM", 24 saat) ve kuyrugun saat dilimine gore cozulur.
slots: [
{ id: 'mon-morning', dayOfWeek: 1, time: '09:30' },
{ id: 'wed-morning', dayOfWeek: 3, time: '09:30' },
{ id: 'fri-afternoon', dayOfWeek: 5, time: '16:00' },
],
// Verilmezse profilin saat dilimi devralinir.
timezone: 'Europe/Istanbul',
},
});
}
{
"id": "b2c3d4e5f6a7b8c9d0e1f2a3",
"profileId": "a1b2c3d4e5f6a7b8c9d0e1f2",
"name": "Editorial",
"timezone": "Europe/Istanbul",
"slots": [
{ "id": "mon-morning", "dayOfWeek": 1, "time": "09:30" },
{ "id": "wed-morning", "dayOfWeek": 3, "time": "09:30" },
{ "id": "fri-afternoon", "dayOfWeek": 5, "time": "16:00" }
]
}
A slot carries exactly three fields and nothing else: id (your own label, no
whitespace), dayOfWeek (0 = Sunday through 6 = Saturday) and time
("HH:MM", 24-hour). At least one slot is required. Two slots may not share an
id (400 duplicate_slot_id), and two may not land on the same weekday and
time (400 duplicate_slot_time) — a duplicated slot would silently double that
day's throughput.
A queue carries at most 1008 slots, which is every ten minutes of every day
of the week (6 × 24 × 7). A rhythm tighter than that is not really a queue — it
is a stream of individually timed posts, and scheduledFor on
Posts is the tool for it. Sending more is
400 too_many_slots; the extra slots are never silently dropped, because a
truncated pattern would publish on a schedule you did not ask for and the
difference would only surface weeks later.
A profile holds at most 100 queues — the same number as one full page of the
queue list. The 101st is 400 too_many_queues; delete a queue first. If a
profile somehow holds more (queues created before this limit existed), the
unpaginated all=true schedule read answers 400 too_many_queues too, rather
than quietly returning the first hundred; read those through listQueues, which
is cursor-paginated. Deleting is never blocked by the limit.
timezone is optional and defaults to the owning profile's zone. Names are
unique per profile: a second "Editorial" is 409 queue_name_taken.
Listing
export async function listQueues(
zhyper: ZhyperClient,
profileId: string,
): Promise<Array<Schemas['QueueDto']>> {
const queues: Array<Schemas['QueueDto']> = [];
for await (const queue of zhyper.paginate<Schemas['QueueDto']>('listQueues', {
query: { profileId, limit: 50 },
})) {
queues.push(queue);
}
return queues;
}
listQueues is cursor-paginated like every other list
(pagination) and filters by
profileId. sort accepts created_at or -created_at, newest first by
default; limit runs 1..100 and defaults to 25.
Seeing where a post would land
export async function previewNextSlots(
zhyper: ZhyperClient,
input: { profileId: string; queueId: string; count: number },
): Promise<string[]> {
const preview = await zhyper.request<
Schemas['QueueSchedulePreviewResponseDto']
>('previewQueue', {
query: {
profileId: input.profileId,
queueId: input.queueId,
count: input.count,
},
});
// Onizleme hicbir sey AYIRMAZ: donen anlar, bugun bos olan slotlardir.
// Iki musteri ayni onizlemeyi okuyup ayni anda kuyruga atarsa ikincisi
// bir SONRAKI slota duser.
return preview.slots ?? [];
}
previewQueue projects the next count slot times (1..100, default 20) as
absolute instants. With the queue above, a Wednesday-morning read returns:
{
"queueId": "b2c3d4e5f6a7b8c9d0e1f2a3",
"queueName": "Editorial",
"count": 3,
"slots": [
"2026-08-12T06:30:00.000Z",
"2026-08-14T13:00:00.000Z",
"2026-08-17T06:30:00.000Z"
]
}
Those are UTC instants for 09:30 and 16:00 Istanbul time. The preview reserves nothing. It is a projection of the pattern, not a hold on the calendar, so two clients that read the same preview and then both enqueue will not collide — the second one simply lands on the following slot.
Enqueuing a post
export async function queuePost(
zhyper: ZhyperClient,
input: { profileId: string; queueId: string; content: string; targets: PostTarget[] },
): Promise<Schemas['CreatePostResponseDto']> {
return zhyper.request<Schemas['CreatePostResponseDto']>('createPost', {
body: {
content: input.content,
platforms: input.targets,
// Kuyruk kipini SECEN alan budur. `queueId` yalnizca profilde tek bir
// kuyruk varsa atlanabilir; birden fazlaysa acikca secmek gerekir.
queuedFromProfile: input.profileId,
queueId: input.queueId,
},
});
}
queuedFromProfile is what selects queue mode; queueId picks which queue, and
may be omitted only when the profile has exactly one. Sending queueId
without queuedFromProfile is 400 queue_id_requires_queue, and combining queue
mode with scheduledFor or publishNow is 400 conflicting_schedule_mode
(Posts).
The created post comes back with scheduleKind: "recurring", the queueId it
drew from, and the queueSlot it was given. Allocation is real, and the next
read shows it:
export async function nextFreeSlot(
zhyper: ZhyperClient,
input: { profileId: string; queueId: string },
): Promise<string | undefined> {
const next = await zhyper.request<Schemas['QueueScheduleNextSlotResponseDto']>(
'getNextQueueSlot',
{ query: { profileId: input.profileId, queueId: input.queueId } },
);
return next.nextSlot;
}
Against the same queue, after the Wednesday slot was taken by the post above,
getNextQueueSlot answers 2026-08-14T13:00:00.000Z — the Friday slot. Preview
does not move; the next-slot pointer does.
An inactive queue answers 400 queue_inactive rather than quietly returning an
empty projection.
The /v1/queue compatibility surface
There are two families of endpoint here and it is worth knowing which you are looking at:
/v1/queues | /v1/queue/slots | |
|---|---|---|
| Shape | Queue objects, cursor-paginated | One profile's schedule |
| Slot fields | id, dayOfWeek, time | dayOfWeek, time |
| Extras | — | active, isDefault, _id |
| Built for | New integrations | Zernio compatibility |
GET /v1/queue/slots requires profileId and returns two different shapes:
all=true gives { queues, count } for the whole profile, and anything else
gives { exists, schedule, nextSlots } for a single queue. Combining all=true
with queueId is rejected (400 invalid_query) rather than silently preferring
one. The all=true shape has no pagination, so it is bounded by the
100-queues-per-profile limit above and answers 400 too_many_queues for a
profile that exceeds it. Its mutations additionally report reshuffledCount and
skippedDailyLimit, which tell you how many already-queued posts moved when the
pattern changed.
Prefer /v1/queues for anything new.
Deleting
deleteQueue refuses while an active scheduled post still points at the queue:
409 queue_in_use. Reschedule or cancel those posts first — see
cancelPendingTargets in Posts.
Errors worth branching on
| Code | Status | Meaning |
|---|---|---|
invalid_slots | 400 | slots missing or empty |
invalid_slot_day_of_week | 400 | Not an integer 0..6 |
invalid_slot_time | 400 | Not HH:MM |
duplicate_slot_id | 400 | Two slots share an id |
duplicate_slot_time | 400 | Two slots share a weekday and time |
too_many_slots | 400 | More than 1008 slots in one queue |
too_many_queues | 400 | The profile is at (or over) 100 queues |
queue_inactive | 400 | The queue exists but is switched off |
profile_not_found | 404 | No such profile for this key |
queue_not_found | 404 | No such queue for this key |
queue_name_taken | 409 | Name already used on that profile |
queue_in_use | 409 | An active scheduled post still points at it |
Next
Media
Media is uploaded before the post that uses it. You get back a media id, and that id is what a post carries — a post never references a URL you host. Sending a media field to creat
Accounts
An account is one connected destination: a Mastodon handle, a Discord channel, a Pinterest board, a LinkedIn page. It belongs to exactly one profile, and it is what a post target p