Dovilo Webhooks

Dovilo can POST a signed JSON event to your endpoint whenever a task in one of your projects changes. Use it to bridge Dovilo into Slack, Linear, your own automation, an LLM agent, anything that can accept an HTTPS request.

This document is the public reference for integrating with Dovilo webhooks. It covers events, payload schema, signature verification, retries, and the operational guarantees you should design your receiver around.

Have an AI agent scaffold the receiver

Integration prompt

Paste this into Claude Code, Cursor, Windsurf, or any other AI coding agent and it will build you a webhook receiver in your project's stack, set up signature verification correctly, and end with a checklist for the Dovilo-side setup.

AI integration prompt
I want to receive task events from Dovilo (https://dovilo.app — a gamified
todo app) into this project and act on them. Please build a webhook
receiver here, then walk me through the Dovilo-side setup.

DO THIS IN ORDER:

────────────────────────────────────────────────────────────────────
STEP 1 — ASK ME FIRST
────────────────────────────────────────────────────────────────────
- What should happen when an event arrives? (Slack notification?
  Linear sync? CI trigger? Database log? Hand off to an LLM?)
- Which stack should the receiver use? Match this project if obvious;
  otherwise ask. (Node/Express, FastAPI, Go net/http, Cloudflare
  Workers, Vercel function, etc.)
- Where will it be deployed and what's the public URL going to be?
  Public HTTPS URLs are delivered by Dovilo's server. A localhost URL
  is delivered by Dovilo Desktop, which must stay open. To test a
  public URL from a dev machine, suggest ngrok / cloudflared /
  tailscale funnel.

────────────────────────────────────────────────────────────────────
STEP 2 — WRITE THE RECEIVER
────────────────────────────────────────────────────────────────────
Route: POST /webhooks/dovilo  (or fit the project's conventions)

CRITICAL RULES:
- Read the RAW request body before any JSON parsing. The HMAC is
  computed over the raw bytes; re-serializing breaks the signature.
- Verify `X-Dovilo-Signature` (format: `t=<unix-ms>,v1=<hex>`):
    1. Parse `t` and `v1`.
    2. Reject if `|Date.now() - t|` > 5 minutes (replay protection).
    3. Compute `HMAC_SHA256(secret, `${t}.${rawBody}`)`, hex-encode.
    4. Compare to `v1` in CONSTANT TIME (timingSafeEqual /
       hmac.compare_digest / hmac.Equal). Never use plain `==`.
- Use `X-Dovilo-Delivery` (also `id` in the body) as the idempotency
  key — store it, drop duplicates. Same id is reused across retries.
- Respond 2xx on success and 401 on a bad signature. NEVER answer a
  bad signature with 410. Respond 410 ONLY if the integration is
  permanently dead: Dovilo disables the project's webhook when it
  sees 410.
- Read the signing secret from env var `DOVILO_WEBHOOK_SECRET`. Add
  a placeholder to `.env.example`. NEVER commit the real secret.

────────────────────────────────────────────────────────────────────
STEP 3 — HANDLE THESE EVENT TYPES
────────────────────────────────────────────────────────────────────
- `task.created`         — new task created.
- `task.status_changed`  — body has `previousStatus` + `task.status`.
                           Status values: scheduled, queued, running,
                           awaiting-user, done, done-with-errors, error.
                           A cancelled task ends in error, with
                           task.cancelledAt set.
- `webhook.ping`         — test event from Dovilo's "Send test" button.
                           Acknowledge with 2xx; don't trigger anything.

Payload shape (slim — these are ALL the fields you get):

{
  "id": "evt_...",                  // also in X-Dovilo-Delivery
  "type": "task.status_changed",
  "createdAt": 1715900000000,       // unix ms when queued
  "projectId": "list_...",
  "projectName": "Marketing site",  // may be omitted
  "previousStatus": "running" | null,  // status_changed only
  "task": {
    "id": "tsk_...",
    "text": "Draft launch announcement",
    "status": "done",
    "projectId": "list_...",
    "createdAt": 1715800000000,
    "updatedAt": 1715900000000,     // USE THIS to order, not delivery time
    "priority": "high",             // optional: none|low|medium|high|urgent
    "importance": true,             // optional
    "scheduledAt": 1715886000000,   // optional, unix ms
    "isRecurring": true,            // optional
    "recurrenceType": "weekly",     // optional: daily|weekly|monthly|yearly
    "seriesId": "tsk_...",          // optional, shared by every run
    "cancelledAt": 1715900000000,   // optional, set when cancelled
    "startedAt": 1715890000000,     // optional
    "completedAt": 1715900000000,   // optional
    "error": { "message": "...", "stepId": "..." },  // optional, on error
    "claimedByAgentId": "agent_...",       // optional
    "claimedByAgentName": "Claude Code",   // optional
    "telemetry": {                  // only on terminal transitions
      "entries": 14, "tokensIn": 12483, "tokensOut": 3201,
      "durationMs": 84120, "costEstimate": 0.0731, "truncated": false
    }
  }
}

Ordering: same task = FIFO. Different tasks = parallel, no order
guarantee. Sort by `task.updatedAt` if you need a total order.

────────────────────────────────────────────────────────────────────
STEP 4 — TEST LOCALLY
────────────────────────────────────────────────────────────────────
Show me how to:
- Run the receiver locally.
- Expose it on a public URL if needed (ngrok / cloudflared).
- What logs to watch.

────────────────────────────────────────────────────────────────────
STEP 5 — END WITH THIS CHECKLIST (substitute the real URL)
────────────────────────────────────────────────────────────────────
Print this checklist VERBATIM at the end, replacing `<YOUR_URL>` with
the actual public URL of the receiver you just built:

  ─────────────────────────────────────────────────────────────
  FINAL STEP — set this up on the Dovilo side:

  1. Open Dovilo Desktop (2.0 or later) and open the List settings
     of the project you want events from.
  2. Find the "Webhooks" section.
  3. Paste this URL into the URL field:
       <YOUR_URL>
  4. Tick the events you want (task.created and/or
     task.status_changed), switch the webhook to "Enabled" and
     press Save.
  5. Click "Generate signing secret". The plaintext secret is
     shown EXACTLY ONCE: copy it immediately.
  6. Put the secret in your `.env` (or your host's secret manager):
       DOVILO_WEBHOOK_SECRET=<paste-here>
     Then restart the receiver so it picks up the env var.
  7. Click "Send test". You should see a 2xx in your receiver logs
     within a second.
  ─────────────────────────────────────────────────────────────

────────────────────────────────────────────────────────────────────
GUARDRAILS
────────────────────────────────────────────────────────────────────
- Don't invent Dovilo features. If I ask for something this prompt
  doesn't cover (inbound API, unsubscribe webhook, batch deliveries,
  task.deleted events), say "not currently supported by Dovilo
  webhooks — check https://dovilo.app/docs/webhooks/" rather than
  guessing or stubbing.
- The webhook payload is intentionally slim. If I need the full task
  (description, agent notes, full telemetry, steps), tell me to
  fetch it via Dovilo's Agents MCP (https://dovilo.app/mcp/agents,
  agent key as a Bearer token) using `get_task` / `list_tasks`.

Tip: for the fastest result, run it inside the project that will host the receiver, so the agent picks up your stack, conventions, and lint/test scripts.

Quick start

  1. In Dovilo Desktop 2.0 or later, open the project's List settings and find the Webhooks section. Webhooks are set up on the desktop only.
  2. Paste your receiver URL. It must be https:// unless it is a local address; see Delivery modes.
  3. Pick which events you want (task.created, task.status_changed), switch the webhook to Enabled and press Save. For a public URL the server needs the saved address before it can create a secret.
  4. Click Generate signing secret. The plaintext is shown once: copy it into your receiver's secret store immediately.
  5. Click Send test to fire a webhook.ping against your endpoint.

Once enabled, every matching task event in that project is delivered as a signed POST to the configured URL.

Delivery modes

Who sends the request depends on the URL. A public HTTPS receiver is delivered by Dovilo's server, so it gets events even when Desktop is closed. A receiver on your own machine or network is delivered by Dovilo Desktop, because Dovilo's server never sends to a private address; the app labels these Local.

Server deliveryDesktop delivery (Local)
Applies toPublic URLslocalhost, *.local, 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, ::1, fc00::/7, fe80::/10
URL schemehttps:// only; a public http:// URL is not savedhttp:// or https://
Desktop must be runningNoYes. Events while Desktop is closed are not resent
Signing secretGenerated on Dovilo's server, stored encrypted, shown onceGenerated on this computer, kept in OS secure storage, shown once
Send testFrom the server; the editor shows the HTTP status your endpoint returnedFrom Desktop
After 410 GoneDelivery stops for that URL until a successful Send test, a new secret or a new URLThe project's webhook is switched to disabled
Failure detailsLast delivery, last HTTP status and last error in the webhook settingsIn-memory failure log in the webhook settings

Signature, headers, payload, retry schedule and per-task ordering are the same in both modes. The server never delivers to a private address, even when a public hostname resolves to one.

Transport

PropertyValue
MethodPOST
Content-Typeapplication/json
User-AgentDovilo-Webhook/1.0
URL schemeshttps://; http:// for local addresses only (Delivery modes)
Body encodingUTF-8 JSON
Per-attempt timeout10 seconds

Anything outside http(s):// is rejected before a request is made. A public http:// URL is not saved, because the server delivers only over HTTPS. Plain http:// to a local address is allowed, with a warning in the UI; don't ship a production integration over cleartext.

Events

Dovilo currently emits three event types. The set is intentionally small; we'd rather add events later than walk back a half-baked one.

Event typeWhen it fires
task.createdA new task is created in the project.
task.status_changedA task's status changes (e.g. queued → running → done).
webhook.pingSynthetic event sent by the Send test button. Single attempt, no retry.

You select which events to subscribe to per project. webhook.ping is always delivered when the test button is pressed, regardless of subscription.

Task lifecycle reference

task.status_changed carries a previousStatus field. The status values you will see in task.status and previousStatus:

scheduled  →  queued  →  running  →  awaiting-user  →  queued  →  running  →  done
                                                                           →  done-with-errors
                                                                           →  error

scheduled is a task waiting for its scheduledAt time. Dovilo moves it to queued when the time comes, so previousStatus can be scheduled. A task created without a future time starts at queued.

awaiting-user is entered when an agent pauses for human input (plan approval, mid-task question). Once the user responds, it goes back to queued, and an agent picks it up again on its next turn.

The terminal statuses are done, done-with-errors and error. A cancelled task ends in error with cancelledAt set; an error without it is a failure. For a repeating task, the next run is a new task with the same seriesId, created once the previous one reaches a terminal status.

Headers

Every delivery includes three Dovilo-specific headers in addition to the standard ones.

HeaderExamplePurpose
X-Dovilo-Signaturet=1715900000000,v1=4b2a...c7HMAC signature + timestamp (see below)
X-Dovilo-Eventtask.status_changedEvent type: route without parsing body
X-Dovilo-Deliveryevt_4f0b8a3e-7d2a-4c92-9b13-3e8a1f6c4d11Unique delivery id: use for deduplication

The delivery id is stable across retries of the same delivery, so it is safe to use as an idempotency key in your receiver.

Payload schema

The body is a JSON object. Field set is deliberately slim: just enough to route and decide whether to fetch more. Large or sensitive fields (description, agentNotes, context, steps, full telemetry) are not included; fetch the full task via MCP (get_task) if you need them.

Top-level

JSON
{
  "id": "evt_4f0b8a3e-7d2a-4c92-9b13-3e8a1f6c4d11",
  "type": "task.status_changed",
  "createdAt": 1715900000000,
  "projectId": "list_2hP9...",
  "projectName": "Marketing site",
  "previousStatus": "running",
  "task": { ... }
}
FieldTypeNotes
idstringSame value as X-Dovilo-Delivery. Idempotency key.
typestringOne of task.created, task.status_changed, webhook.ping.
createdAtnumber (unix ms)Time the event was queued for delivery, not the task time.
projectIdstringDovilo project (custom list) id.
projectNamestring | undefinedDisplay label resolved at fire time. Omitted if project was deleted.
previousStatusstring | nullOnly present for task.status_changed. null on first transition.
taskobjectSlim task: see below. Absent on webhook.ping.

task object

JSON
{
  "id": "tsk_8a1c...",
  "text": "Draft launch announcement",
  "status": "done",
  "projectId": "list_2hP9...",
  "createdAt": 1715800000000,
  "updatedAt": 1715900000000,
  "priority": "high",
  "importance": true,
  "scheduledAt": 1715886000000,
  "isRecurring": true,
  "recurrenceType": "weekly",
  "seriesId": "tsk_7f02...",
  "startedAt": 1715890000000,
  "completedAt": 1715900000000,
  "error": { "message": "...", "stepId": "step_..." },
  "claimedByAgentId": "agent_4d...",
  "claimedByAgentName": "Claude Code",
  "telemetry": {
    "entries": 14,
    "tokensIn": 12483,
    "tokensOut": 3201,
    "durationMs": 84120,
    "costEstimate": 0.0731,
    "truncated": false
  }
}
FieldTypeNotes
idstringTask id.
textstringTask title.
statusstringCurrent status (see lifecycle reference).
projectIdstringMirrors top-level projectId.
createdAtnumber (unix ms)Task creation time.
updatedAtnumber (unix ms)Use this, not the event arrival time, to order events for the same task.
prioritystring | omittednone | low | medium | high | urgent.
importanceboolean | omitted"Starred" flag.
scheduledAtnumber | omittedWhen the task is (or was) scheduled to start, unix ms (UTC).
isRecurringboolean | omittedtrue while the task repeats; false once repeating was stopped.
recurrenceTypestring | omitteddaily | weekly | monthly | yearly.
seriesIdstring | omittedShared by every run of a repeating task (the first run's task id).
cancelledAtnumber | omittedSet with status error when the task was cancelled, including a single run of a repeating task that the user skipped, unix ms. An error without it is a failure. Occurrences missed while an earlier run was still open are never created, so they send no event.
startedAtnumber | omittedWhen the task first entered running.
completedAtnumber | omittedWhen the task reached a terminal status.
errorobject | omitted{ message, stepId? }. Present on error, sometimes carried after recovery.
claimedByAgentIdstring | omittedId of the agent that picked up the task.
claimedByAgentNamestring | omittedDisplay name resolved at fire time.
telemetryobject | omittedOnly on terminal transitions (done, done-with-errors, error). Aggregated totals.

Fields with omitted mean: the key is absent from the JSON, not present with a null value. Your receiver should treat missing keys as "not set."

webhook.ping payload

The test delivery uses a stripped-down body:

JSON
{
  "id": "evt_...",
  "type": "webhook.ping",
  "createdAt": 1715900000000,
  "projectId": "list_2hP9..."
}

No task, no signature differences: same headers, same HMAC scheme.

Signature verification

Every delivery is signed with an HMAC-SHA256 of {timestamp}.{rawBody} using the per-project signing secret you generated in the UI. The format is modelled after Stripe's so it should look familiar.

Algorithm

  1. Read X-Dovilo-Signature. It looks like t=1715900000000,v1=4b2a...c7.
  2. Parse out the timestamp (t, unix ms) and the v1 signature (v1, hex).
  3. Reject if |now - t| exceeds the 5-minute replay window.
  4. Compute HMAC_SHA256(secret, "{t}.{rawBody}") and hex-encode.
  5. Compare the computed hex to v1 in constant time.

Critical: Use the raw, unparsed request body bytes. Frameworks that auto-parse JSON often discard the original bytes; re-serializing the parsed object will produce a different signature.

Node.js (Express)

JavaScript
const crypto = require('node:crypto');
const express = require('express');

const app = express();

// Keep the raw body around — JSON.parse re-serialization breaks the HMAC.
app.use(
  '/webhooks/dovilo',
  express.raw({ type: 'application/json' }),
);

const SECRET = process.env.DOVILO_WEBHOOK_SECRET;
const REPLAY_WINDOW_MS = 5 * 60 * 1000;

function verifyDoviloSignature(req) {
  const header = req.get('x-dovilo-signature');
  if (!header) return false;

  const parts = Object.fromEntries(
    header.split(',').map((p) => {
      const i = p.indexOf('=');
      return [p.slice(0, i).trim(), p.slice(i + 1)];
    }),
  );
  const t = Number(parts.t);
  const v1 = parts.v1;
  if (!Number.isFinite(t) || !v1) return false;

  if (Math.abs(Date.now() - t) > REPLAY_WINDOW_MS) return false;

  const expected = crypto
    .createHmac('sha256', SECRET)
    .update(`${t}.${req.body.toString('utf8')}`)
    .digest('hex');

  const a = Buffer.from(expected, 'utf8');
  const b = Buffer.from(v1, 'utf8');
  if (a.length !== b.length) return false;
  return crypto.timingSafeEqual(a, b);
}

app.post('/webhooks/dovilo', (req, res) => {
  if (!verifyDoviloSignature(req)) {
    return res.status(401).send('invalid signature');
  }
  const event = JSON.parse(req.body.toString('utf8'));
  // ... handle event ...
  res.status(204).end();
});

Python (Flask)

Python
import hmac
import hashlib
import time
from flask import Flask, request, abort

app = Flask(__name__)
SECRET = os.environ["DOVILO_WEBHOOK_SECRET"].encode("utf-8")
REPLAY_WINDOW_MS = 5 * 60 * 1000

def verify_dovilo_signature(req) -> bool:
    header = req.headers.get("X-Dovilo-Signature", "")
    parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
    try:
        t = int(parts["t"])
        v1 = parts["v1"]
    except (KeyError, ValueError):
        return False

    if abs(int(time.time() * 1000) - t) > REPLAY_WINDOW_MS:
        return False

    raw = req.get_data()  # bytes — do NOT use request.json
    mac = hmac.new(SECRET, f"{t}.".encode("utf-8") + raw, hashlib.sha256)
    return hmac.compare_digest(mac.hexdigest(), v1)

@app.post("/webhooks/dovilo")
def dovilo_webhook():
    if not verify_dovilo_signature(request):
        abort(401)
    event = request.get_json(force=True)
    # ... handle event ...
    return "", 204

Go

Go
package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "io"
    "net/http"
    "os"
    "strconv"
    "strings"
    "time"
)

const replayWindow = 5 * time.Minute

func verify(r *http.Request, secret []byte, body []byte) bool {
    header := r.Header.Get("X-Dovilo-Signature")
    var t int64
    var v1 string
    for _, p := range strings.Split(header, ",") {
        kv := strings.SplitN(p, "=", 2)
        if len(kv) != 2 { continue }
        switch kv[0] {
        case "t":
            n, err := strconv.ParseInt(kv[1], 10, 64)
            if err != nil { return false }
            t = n
        case "v1":
            v1 = kv[1]
        }
    }
    if t == 0 || v1 == "" { return false }

    now := time.Now().UnixMilli()
    if abs(now-t) > replayWindow.Milliseconds() { return false }

    mac := hmac.New(sha256.New, secret)
    mac.Write([]byte(strconv.FormatInt(t, 10) + "."))
    mac.Write(body)
    expected := hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(expected), []byte(v1))
}

func handle(w http.ResponseWriter, r *http.Request) {
    secret := []byte(os.Getenv("DOVILO_WEBHOOK_SECRET"))
    body, _ := io.ReadAll(r.Body)
    if !verify(r, secret, body) {
        http.Error(w, "invalid signature", http.StatusUnauthorized)
        return
    }
    // ... handle event ...
    w.WriteHeader(http.StatusNoContent)
}

func abs(x int64) int64 { if x < 0 { return -x }; return x }

Common verification mistakes

  • Re-serializing the body. If you JSON.parse → JSON.stringify before hashing, byte-for-byte differences (key order, whitespace) will break the signature. Hash the raw bytes.
  • String comparison instead of constant time. Use timingSafeEqual / hmac.compare_digest / hmac.Equal. Plain == leaks timing info.
  • Skipping the timestamp check. Without it, a captured payload can be replayed forever. Enforce the 5-minute window.
  • Trusting the body before verifying. Verify the signature on the raw bytes first, then parse the JSON.

Responses Dovilo expects

Your receiver's HTTP status determines what happens next.

Status rangeDovilo's behavior
2xxDelivered. Done.
429 Too Many RequestsTreated as transient. Retried per the schedule below. Retry-After is surfaced in failure logs but the next attempt still comes from the standard backoff.
4xx (other than 429)Permanent failure. No retry. Recorded in the failures buffer.
410 GonePermanent failure and Dovilo disables the webhook for this project.
5xxTreated as transient. Retried per the schedule below.
Connection error / DNS / TLSTreated as transient. Retried.
Timeout (>10s)Treated as transient. Retried.

Respond quickly: under a few hundred ms is ideal. If your processing is slow, accept the delivery (202) and queue the work asynchronously rather than holding the request open.

Use 410 Gone deliberately. It is the documented "this endpoint is dead, stop sending" signal. What happens next depends on the delivery mode: Desktop switches the project's webhook to disabled, and the server stops delivering to that URL.

Answer an invalid signature with 401, never 410. A request that fails verification may come from an older Dovilo Desktop still signing with a secret you have since replaced. 410 Gone disables the subscription, so a 410 sent to that request would switch off the webhook for the whole project, server delivery included.

Retry policy

Failed transient deliveries are retried up to 5 times on this schedule:

AttemptWait before attempt
10
25 seconds
330 seconds
42 minutes
510 minutes
61 hour

Total delivery window is therefore approximately 1 hour 15 minutes. After the final attempt, the delivery is dropped.

  • Server delivery: the webhook settings show the last delivery, its HTTP status and the last error.
  • Desktop delivery: the failure is recorded in the failures ring buffer in the webhook settings (last 100 entries per project). The buffer is in-memory on the desktop client and is not persisted across app restarts, so treat it as a debugging aid, not an audit log. A pending retry is lost if Desktop quits.

Ordering and concurrency

  • Same task: events are delivered in FIFO order. A retry of an older event will not pass a newer event for the same task.
  • Different tasks: events run in parallel. There is no ordering guarantee across tasks.
  • Receivers should not rely on delivery time. Use task.updatedAt (and previousStatus for transitions) to reconstruct order.

Idempotency

Use X-Dovilo-Delivery (also available as id in the body) as your idempotency key. The same delivery id is reused across retries of the same event, so deduplication on this key is both safe and correct.

Recommended pattern: insert the delivery id into a unique-keyed table inside the same transaction as your side effects. On retry, the duplicate key collision becomes a no-op.

Security

  • The signing secret is the only thing standing between your endpoint and a spoofed Dovilo request. Treat it like a password.
  • Anyone who learns the secret can forge requests that pass verification.
  • Rotate the secret if you suspect compromise: generating a new one in the UI immediately invalidates the previous secret.
  • Plaintext is revealed exactly once, when generated. There is no way to retrieve it later: generate, copy into your secret store, move on.
  • Server delivery: the secret is generated and stored by Dovilo's server, encrypted. It is never written where the apps can read it back.
  • Desktop delivery: the secret is stored by the desktop client using OS-level secure storage (Keychain / Credential Manager / libsecret). It is not written to Firestore and is not synced across devices.

Operational notes

  • Desktop-bound delivery (Local only). Local webhooks fire from the Dovilo desktop app process, so they will not deliver when the app is closed. Events that occur while the app is closed are not queued for later delivery; they are simply not sent. Tasks already present in Firestore at app launch are treated as historical state, not new events, and do not fire.
  • Mobile- and agent-originated changes fire too. Server delivery observes every change. For Desktop delivery, while the desktop is running, any task created or transitioned by mobile or an MCP agent is observed via Firestore sync and fires the same events as a local write. The desktop persists per-task dedup markers (webhookCreatedFiredAt, webhookLastSentStatus) inside the same Firestore write, so a snapshot echo or a second connected desktop observing the same change will not double-fire.
  • Multi-desktop dedup (Desktop delivery). When two desktops are signed in to the same account they race for each cloud-driven event via a Firestore transactional compare-and-swap on the dedup markers. Only the winner delivers, so receivers do not need to dedup across desktops on top of X-Dovilo-Delivery.
  • Slim payloads on purpose. Bulky and sensitive fields are excluded to reduce both bandwidth and the chance of leaking prompt content into third-party integrations. Fetch the full task with get_task or list_tasks over Agents MCP (https://dovilo.app/mcp/agents, agent key as Authorization: Bearer), which works while Desktop is closed.
  • No receiver? Poll list_tasks over Agents MCP with updated_after set to the largest updatedAt from the previous response.
  • No webhook events for deleted tasks. A task.deleted event is not currently emitted. If you need to know about deletions, reconcile periodically through MCP.

Changelog

  • v0.6.0 — Initial outbound webhook support. Events: task.created, task.status_changed, webhook.ping. HMAC-SHA256 signing. 5-attempt retry. Per-task FIFO.
  • v0.6.x — Mobile- and agent-driven changes now fire via Firestore sync diff (previously only desktop-initiated writes fired). Persisted per-task dedup markers prevent snapshot-echo refire and double-fire across multiple connected desktops.
  • Dovilo 2.0: Server delivery for public https:// URLs: signing secret, test and delivery status from the server, and delivery while Desktop is closed. Local URLs stay with Desktop. Public http:// URLs are not accepted.
  • Dovilo 2.0: Scheduled and repeating tasks. New status value scheduled, so previousStatus can be scheduled. Task payload: scheduledAt, isRecurring, recurrenceType and seriesId added; receivers that ignore unknown keys need no change for these. dueAt removed: it was not in use, but a receiver that read it should stop.
  • Dovilo 2.0: cancelledAt (unix ms) on cancelled tasks, including a skipped run of a repeating task, so a cancel can be told apart from a failure without parsing error.message. Additive.
  • Correction: the terminal status values are done, done-with-errors and error. Earlier versions of this page listed failed and cancelled; a receiver that compares task.status against those strings should check for error, and for cancelledAt, instead.