Integration API

Machine-to-machine API for partner systems. YDONE is the source of truth; partner systems keep a cache and reconcile through delta sync and signed webhooks.

Base URL

https://ydone.app

Preview/staging: https://project--6c597736-5b05-4ad7-8bd1-20789c8ec13a-dev.lovable.app

Authentication

Every request (except /health) carries three headers: x-ydone-tenant, x-ydone-timestamp (unix seconds, max 5 minutes old) and x-ydone-signature — the hex HMAC-SHA256 of `${timestamp}.${rawBody}` using the shared secret PARTNER_API_SECRET. Signatures are compared in constant time.

Server-to-server only — never in the browser

The tenant secret is a master key for every task of that tenant. Store it in your backend secret store (e.g. Lovable Cloud Secrets) and sign requests exclusively in server-side code — an edge/server function. Your frontend must never talk to YDONE directly; it calls your own authenticated backend endpoint, which then calls YDONE. Requests that look like they originate from a browser (any Origin or Sec-Fetch-* header) are rejected with 403 browser_client_forbidden, and this API returns no CORS headers. If a secret ever reached client code, rotate it immediately in YDONE.

import crypto from "node:crypto";

const BASE_URL = "https://ydone.app";
// Each tenant has its own HMAC secret. Rotate it in YDONE Platform admin → Partner portals.
const SECRET = process.env.YDONE_TENANT_SECRET;
const TENANT = "jsc-solutions";

async function call(path, method = "GET", payload) {
  const rawBody = payload ? JSON.stringify(payload) : "";
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const signature = crypto
    .createHmac("sha256", SECRET)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");

  const res = await fetch(BASE_URL + path, {
    method,
    headers: {
      "content-type": "application/json",
      "x-ydone-tenant": TENANT,
      "x-ydone-timestamp": timestamp,
      "x-ydone-signature": signature,
    },
    ...(rawBody ? { body: rawBody } : {}),
  });
  return res.json();
}

Endpoints

GET/api/public/v1/healthLiveness probe — no signature required
POST/api/public/v1/tasks/upsertBatch upsert (max 200) keyed on (external_system, external_id)
GET/api/public/v1/tasks?since=&limit=&cursor=Delta sync incl. soft-deleted tasks (deleted: true)
GET/api/public/v1/tasks/:idSingle task
POST/api/public/v1/tasks/:id/statusLightweight status change with optional comment
DELETE/api/public/v1/tasks/:idSoft delete
POST/api/public/v1/activation-linksCreate a one-time YDONE activation link for an organization admin
GET/api/public/v1/activation-linksList activation links of your tenant (no tokens)
POST/api/public/v1/activation-links/:id/revokeRevoke an unused activation link
POST/api/public/v1/workspaces/upsertMirror partner areas (phase 2)
GET/api/public/v1/workspacesList mirrored areas (phase 2)

Upsert payload

POST /api/public/v1/tasks/upsert

{
  "tenant": "jsc-solutions",
  "tasks": [
    {
      "external_id": "0f8f2c9e-2f1a-4f21-9a0e-2b6a1d6f9a11",
      "source_module": "ems_gap_item",
      "source_ref_id": "GAP-2024-118",
      "source_ref_label": "Gap 118 — Missing calibration record",
      "source_url": "https://jsc.example.com/ems/gaps/118",
      "title": "Recalibrate torque wrenches in line 3",
      "description": "Calibration overdue since audit finding 118.",
      "status": "in_progress",
      "priority": "high",
      "category": "Quality",
      "assignee_name": "Mara Kessler",
      "assignee_email": "mara.kessler@example.com",
      "start_date": "2026-08-20",
      "due_date": "2026-09-15",
      "progress": 40,
      "tags": ["audit", "calibration"],
      "root_cause": "No recurring calibration schedule",
      "effectiveness_check": "Audit re-check in 90 days",
      "effectiveness_result": null,
      "completed_at": null
    }
  ]
}

200 OK
{
  "results": [
    { "external_id": "0f8f…9a11", "id": "6a1c…", "action": "created", "updated_at": "2026-08-23T21:40:11.204Z" }
  ]
}

action is created, updated or unchanged. Repeating the same payload is idempotent thanks to the unique index on (external_system, external_id).

Delta sync

GET /api/public/v1/tasks?since=2026-08-01T00:00:00Z&limit=100

200 OK
{
  "tasks": [ { "id": "…", "external_id": "…", "status": "done", "deleted": false, "updated_at": "…" } ],
  "next_cursor": "2026-08-23T21:40:11.204Z",
  "has_more": false,
  "server_time": "2026-08-23T21:41:00.000Z"
}

Activation links (portal → YDONE access)

A connected portal can hand an organization admin direct YDONE access. Create a one-time link, show it as a button in your portal ("Open in YDONE") or email it. The link is bound to one email address, expires after 14 days by default and can be revoked while unused. The recipient signs in — or registers with any provider — using that email and is added to the mapped YDONE workspace with the requested role. Only the hash of the token is stored; the plain token is returned exactly once.

POST /api/public/v1/activation-links

{
  "tenant": "jsc-solutions",
  "email": "admin@kunde.de",
  "role": "admin",
  "display_name": "Mara Kessler",
  "external_user_id": "portal-user-4711",
  "expires_in_days": 14
}

201 Created
{
  "activation_link": {
    "id": "…",
    "email": "admin@kunde.de",
    "role": "admin",
    "expires_at": "2026-09-07T09:15:00.000Z",
    "url": "https://ydone.app/activate?token=8Jd…",
    "token": "8Jd…"
  }
}

# revoke an unused link
POST /api/public/v1/activation-links/<id>/revoke
# list links (optional ?email=)
GET  /api/public/v1/activation-links

Webhooks (return channel)

Events: task.created, task.updated, task.status_changed, task.deleted. Same HMAC scheme, signed with the per-target webhook secret. Failed deliveries are retried three times with exponential backoff and logged.

POST <your target_url>
x-ydone-event: task.status_changed
x-ydone-timestamp: 1789251611
x-ydone-signature: <hmac-sha256 of "timestamp.rawBody" with the webhook secret>

{ "event": "task.status_changed", "task": { "id": "…", "external_id": "…", "status": "done" } }

Error codes

401missing_auth_headerstenant, timestamp or signature header missing
401invalid_timestamptimestamp is not unix seconds
401timestamp_expiredtimestamp older/newer than 5 minutes
401invalid_signatureHMAC mismatch
403tenant_not_allowedunknown or deactivated tenant
403tenant_mismatchbody tenant ≠ signed header tenant
400invalid_json / invalid_id / invalid_sincemalformed request
422validation_failedpayload violates schema (see issues[])
404task_not_foundtask does not exist for this tenant
404not_foundactivation link unknown, already redeemed or not yours
500query_failed / update_failed / delete_failedserver-side error
503api_not_configuredsigning secret not configured