Start your launch Sign in
APIs 3 min read

The API task loop

One GET for the day's work, one POST per action taken, one POST for what happened after. Three calls, and the third is what makes tomorrow's list smarter.

Last updated July 25, 2026

The whole daily flow is three endpoints: pull tasks, draft the one you are about to work, and write back what happened. This page walks the loop in order, with the exact shapes.

01 · GET /v1/tasksThe pull

GET /v1/tasks returns your work across every warmup and scout, sorted the way the product sorts it (value_rank ascending, best first). Filter with query params:

  • warmup_id or scout_id: one account’s queue, the usual fleet pattern.
  • status: most scripts want ready. Also accepted: pending, scheduled, locked, done, skipped.
  • since: only tasks created after an ISO 8601 timestamp, for incremental pulls.
  • available_before: everything actionable by a given time, including scheduled tasks whose unlock lands before it.
  • limit: default 100, max 200. The response echoes limit back, so tasks.length === limit means there may be more.
A task (abridged)
{
	"id": "b7e41d80-52c6-4f0a-9c1e-7d3a9e2f5c88",
	"status": "ready",
	"mode": "thread_comment",
	"warmup_id": "3f2b7c1e-9d5a-4f42-8f6e-2a91d0c47b13",
	"scout_id": null,
	"value_rank": 12,
	"why": "A direct question in r/selfhosted that your product answers.",
	"target": { "url": "https://www.reddit.com/r/selfhosted/comments/..." },
	"draft_state": "ready",
	"generated_asset": { "comment": "Been running exactly this setup for a while..." },
	"available_at": null,
	"created_at": "2026-07-26T08:00:12.000Z",
	"completed_at": null
}

The row carries more fields than shown (venue metadata, the stored result of past write-backs); everything is additive, so ignore what you do not need. target holds the destination, always including the thread url for comment work. why is the one-line reason the engine picked it, worth logging next to whatever you post.

02 · DraftingDrafts are two steps

The pull never writes copy inline: drafting is a real model call, and drafting twelve tasks you might not action is exactly the waste the lazy tail exists to prevent. Tasks arrive with draft_state of ready (text included) or lazy (generated_asset is null). For a lazy task, ask for the draft at the moment you need it:

The daily loop
const { tasks } = await nilkick('GET', '/v1/tasks?scout_id=SCOUT_ID&status=ready');

for (const task of tasks) {
	// Draft only what you are about to work. Skipped tasks never cost anything.
	let asset = task.generated_asset;
	if (task.draft_state === 'lazy') {
		({ generated_asset: asset } = await nilkick('POST', `/v1/tasks/${task.id}/draft`, {
			action: 'lazy'
		}));
	}

	// Your side, your infrastructure.
	const url = await postWithYourStack(task.target.url, asset.comment);

	// Tell Nilkick what happened. This is what drives the adaptation.
	await nilkick('POST', `/v1/tasks/${task.id}/status`, {
		status: 'done',
		result: { comment_url: url }
	});
}
Draft one task at a time

Never draft the whole batch up front. Draft immediately before acting: the couple of seconds hide inside browser work you are already doing, skipped tasks stay free, and the draft endpoint’s tighter rate budget never gets in your way. The call is idempotent, so a retry on a task that already has text costs nothing.

Drafted copy lives in generated_asset: comment for replies, post_title and post_body for post-shaped work. Warm-up drafts are written ahead of time by the warmer itself, so the lazy tail is mostly a scout concern.

03 · Write-backsReporting status

POST /v1/tasks/{id}/status with { "status": "done" }, "skipped", or "ready" (which restores a task you marked by mistake). When you posted something, include where: { "status": "done", "result": { "comment_url": "..." } }. The URL is how the outcome pass finds the comment later.

The response is { ok, status, unlock_at }. A non-null unlock_at means completing this task started a timer on a held sibling, the everyday case being an account’s second comment of the day unlocking six hours after the first. That held task shows up in pulls as status: "scheduled" with available_at set, so a single morning fetch under-collects by design: queue the afternoon slot from the timestamp, use available_before, or let the webhook ping you.

04 · OutcomesOutcomes are the adaptation

About a day after posting, check whether each comment survived and report it: POST /v1/tasks/{id}/outcome with { "status": "live" }, "removed", or "unknown". The response returns the applied result and, on a removal, cooldown_until plus how many queued tasks in that subreddit were pulled back.

Treat this as a requirement of a correct integration, not an optional field. Outcome reports drive removal cooldowns, timed subreddit exclusions, standing re-assessment, and the never-offer-the-same-thread-twice guarantee. An integration that only reads gets a static list and none of the adaptation, which is exactly the part you cannot get from a prompt. The reference executor shows a clean way to run the check as a second daily pass.


FAQ

Common questions

Those tasks are in the lazy tail: draft_state is lazy, meaning “not written yet, ask when you need it”. POST to the draft endpoint for the one you are about to work and the text arrives in a couple of seconds. Once drafted it is stored, so later pulls return it as ready.