Utvecklare · Enterprise

Tidvis Staff

REST API v1 och webhooks för att starta bemanningsuppdrag, låta AI ringa kandidater och få strukturerade resultat tillbaka.

Snabbstart

Base URL: /api/public/v1. Alla anrop använder JSON. API:t är en del av Enterprise-paketet och kräver en utfärdad API-nyckel.

  1. 1. Be Tidvis om en API-nyckel kopplad till er organisation.
  2. 2. Skicka ert första bemanningsuppdrag med POST /staffing-requests.
  3. 3. Konfigurera er webhook-endpoint så ni får tillbaka resultatet.
curl -X POST https://tidvis-staff.lovable.app/api/public/v1/staffing-requests \
  -H "Authorization: Bearer $TIDVIS_STAFF_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: shift_83922-2026-06-23" \
  -d '{
    "externalId": "shift_83922",
    "organizationId": "org_123",
    "shift": {
      "id": "shift_83922",
      "startTime": "2026-06-24T08:00:00+02:00",
      "endTime": "2026-06-24T16:00:00+02:00",
      "location": "Stockholm",
      "residentName": "Anna",
      "role": "Personlig assistent"
    },
    "candidates": [
      { "id": "user_1", "name": "Sara",   "phone": "+46701234567", "priority": 1, "allowedToWork": true },
      { "id": "user_2", "name": "Mikael", "phone": "+46707654321", "priority": 2, "allowedToWork": true }
    ],
    "rules": {
      "maxAcceptedWorkers": 1,
      "requireManagerApproval": true,
      "callTimeoutSeconds": 25,
      "maxAttemptsPerCandidate": 2,
      "stopWhenAccepted": true
    },
    "webhookUrl": "https://tidvis.se/api/staffing-webhook"
  }'

Direkt-svar:

{
  "staffingRequestId": "sr_abc123",
  "status": "queued",
  "message": "Staffing request created and calling will begin shortly."
}

Autentisering

Skicka ert API-nyckel som Authorization: Bearer <key> på alla anrop. Nyckeln är kopplad till en organisation och en webhook-signeringshemlighet.

Nyckeln har prefixet tvv_live_ i produktion och tvv_test_ i sandlådan. Återanvänd inte samma nyckel mellan miljöer.

Verktyg & hälsa

Tre hjälp-endpoints för uptime-monitorering och felsökning.

GET/api/public/v1/healthz

Ingen auth. Returnerar {ok:true,...}. Lämplig för uptime-monitor.

GET/api/public/v1/me

Returnerar nyckelns organisation och behörigheter. Använd för att verifiera nyckelinstallation.

POST/api/public/v1/staffing-requests/:id/test-webhook

Skickar en signerad ping-payload till uppdragets webhookUrl så ni kan verifiera HMAC-implementationen.

Staffing requests

POST/api/public/v1/staffing-requests

Skapa ett bemanningsuppdrag. Returnerar staffingRequestId och status queued.

GET/api/public/v1/staffing-requests/:id

Hämta ett bemanningsuppdrag inklusive calls och nuvarande resultat.

{
  "id": "sr_abc123",
  "status": "requires_review",
  "shiftId": "shift_83922",
  "result": {
    "outcome": "candidate_accepted",
    "selectedCandidate": { "id": "user_1", "name": "Sara", "phone": "+46701234567" },
    "confidence": 0.94,
    "requiresManagerApproval": true
  },
  "calls": [
    {
      "candidateId": "user_1",
      "status": "accepted",
      "startedAt": "2026-06-23T14:02:11+02:00",
      "endedAt": "2026-06-23T14:02:58+02:00",
      "durationSeconds": 47,
      "summary": "Sara tackade ja till passet.",
      "transcript": "Hej Sara... Ja, jag kan jobba det passet."
    }
  ]
}
POST/api/public/v1/staffing-requests/:id/cancel

Avbryt ett pågående uppdrag. Inga fler kandidater ringas.

POST/api/public/v1/staffing-requests/:id/retry

Försök igen från första olästa kandidaten. Tillgänglig när status är failed eller no_answer.

GET/api/public/v1/staffing-requests/:id/calls

Lista alla call attempts för ett uppdrag.

GET/api/public/v1/staffing-requests/:id/events

Audit-trail för uppdraget: skapelse, varje statusövergång, webhook-utskick.

Tidvis pass-dialog

När en användare klickar "Be Tidvis AI ringa" i pass-dialogen postar Tidvis nedanstående JSON till /api/public/v1/staffing-requests. Kandidaten svarar med fri röst på svenska — vi spelar in, transkriberar och klassificerar intent (accepted/declined/callback/unclear). Inga tonval används.

{
  "external_request_id": "tidvis-pass-12345",
  "mode": "cover",
  "shift": {
    "startTime": "2026-08-05T06:00:00+02:00",
    "endTime":   "2026-08-05T14:00:00+02:00",
    "location":  "Stockholm"
  },
  "customer": {
    "name": "Mathan Cohen",
    "primaryContact": { "name": "Ingela Åberg", "phone": "+46467300094" }
  },
  "candidates": [
    {
      "id": "tidvis-cand-9981",
      "name": "Maya Engdahl",
      "phone": "+46738992544",
      "category": "ordinarie",
      "hourlyRateOre": 13591
    }
  ],
  "webhookUrl": "https://app.tidvis.se/api/staff-callbacks"
}

mode är cover (lösa pass) eller interest (intresseanmälan). external_request_id är Tidvis pass-id och används för idempotens — samma id returnerar befintligt uppdrag istället för att skapa nytt. category är ordinarie / anhorig / tim.

Webhook tillbaka per kandidatsvar

{
  "event": "candidate.responded",
  "staffingRequestId": "sr_abc123",
  "externalId": "tidvis-pass-12345",
  "externalCandidateId": "tidvis-cand-9981",
  "candidate": { "id": "tidvis-cand-9981", "name": "Maya Engdahl", "phone": "+46738992544" },
  "intent": "accepted",
  "confidence": 0.92,
  "transcript": "Ja, jag kan ta passet",
  "note": "Vill bli uppringd igen efter klockan 17",
  "recordingUrl": "https://api.46elks.com/.../r-xxx.mp3",
  "answeredAt": "2026-08-05T05:42:11+02:00"
}

Matcha mot Tidvis-pass via externalId + externalCandidateId. SMS-utskick hanteras av Tidvis själv — Tidvis AI ringer.

Statusmodell

Ett bemanningsuppdrag rör sig mellan följande statusar. Webhooks postas vid varje övergång.

queued
  → calling
      → candidate_accepted   (stopWhenAccepted=true → completed eller requires_review)
      → candidate_declined   (ringer nästa)
      → no_answer            (ringer nästa, eller failed när listan är slut)
      → requires_review      (låg confidence eller manager-godkännande krävs)
  → cancelled (manuellt avbrutet)
  → completed (alla regler uppfyllda)
  → failed    (ingen kandidat tillgänglig)

Calls

Varje samtalsförsök loggas som ett CallAttempt med transkribering, AI-tolkning och tidsstämplar. Transkribering är opt-in per organisation.

{
  "id": "ca_88...",
  "candidateId": "user_1",
  "providerCallId": "call_xxxxxxxx",
  "status": "accepted",
  "startedAt": "2026-06-23T14:02:11+02:00",
  "endedAt": "2026-06-23T14:02:58+02:00",
  "durationSeconds": 47,
  "summary": "Sara tackade ja till passet.",
  "transcript": "Hej Sara... Ja, jag kan jobba det passet.",
  "intent": "accepted",
  "confidence": 0.94
}

AI-intents

AI:n returnerar alltid en av följande intents. Den fattar inga andra beslut.

  • acceptedTackar ja till passet
  • declinedTackar nej
  • maybeTveksam, behöver tänka
  • wrong_personFel mottagare svarade
  • asked_questionVill veta mer innan beslut
  • needs_callbackBe att bli ringd senare
  • unclearSvaret går inte att tolka
  • angry_or_sensitiveEskalera till människa

Webhooks

Tidvis Staff postar event till er webhookUrl. Payloaden signeras med HMAC-SHA256 över `${timestamp}.${rawBody}` med er per-organisations webhook_secret. Verifiera signaturen och att tidsstämpeln är färsk (rekommenderat: max 300s drift) innan ni behandlar eventet — det skyddar mot replay.

Header: X-Tidvis-Signature: t=<unix>,v1=<hex> samt X-Tidvis-Timestamp: <unix>. Vi gör retries med exponentiell backoff vid svar utanför 2xx.

Retry-policy: 1m, 5m, 30m, 2h, 12h. Efter 5 misslyckade försök markeras leveransen som given_up. Varje leverans har även headern X-Tidvis-Delivery-Id som ni kan logga för felsökning.

Verifiera signaturen så här (Node):

import { createHmac, timingSafeEqual } from "node:crypto";

function verify(rawBody: string, header: string, secret: string) {
  const m = /^t=(\d+),v1=([a-f0-9]+)$/.exec(header);
  if (!m) return false;
  const [, ts, sig] = m;
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // replay-skydd
  const expected = createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
  const a = Buffer.from(sig);
  const b = Buffer.from(expected);
  return a.length === b.length && timingSafeEqual(a, b);
}
POST https://tidvis.se/api/staffing-webhook
{
  "event": "candidate.accepted",
  "staffingRequestId": "sr_abc123",
  "externalId": "shift_83922",
  "candidate": { "id": "user_1", "name": "Sara", "phone": "+46701234567" },
  "result": {
    "answer": "accepted",
    "confidence": 0.94,
    "requiresReview": true,
    "summary": "Sara tackade ja till passet."
  }
}

Event-typer:

  • staffing.queued
  • staffing.calling
  • candidate.accepted
  • candidate.declined
  • candidate.no_answer
  • staffing.requires_review
  • staffing.completed
  • staffing.failed
  • staffing.cancelled

Idempotency

Skicka Idempotency-Key: <unikt-värde>POST /staffing-requestsför säker retry. Vi cachar svaret i 24 timmar; identiska anrop returnerar samma svar med headern Idempotent-Replayed: true.

Rekommendation: använd ert eget shift_id + datum eller en UUID per logiskt uppdrag.

CORS

Endpoints under /api/public/v1 svarar på OPTIONS-preflight med tillåtna metoder, Authorization, Content-Type, Idempotency-Key och X-Request-Id. Per default tillåts alla origins (*); per API-nyckel kan ni låsa till specifika origins (allowedOrigins).

Skydd & GDPR

  • · Samtal spelas bara in när ni har laglig grund. Inspelning är opt-in per organisation.
  • · Transkribering kan stängas av – då tas svaret som ren AI-intent utan att text sparas.
  • · AI:n får aldrig avslöja känsliga brukaruppgifter mer än vad samtalet kräver.
  • · Kandidater kan säga ”ring inte igen”. Beslutet loggas och respekteras i alla uppdrag.
  • · Alla beslut loggas i en audit-trail åtkomlig via GET /staffing-requests/:id/events.

Felkoder

KodBetydelse
400 invalid_requestPayloaden klarade inte Zod-validering.
401 unauthorizedBearer-token saknas eller är ogiltig.
403 forbiddenNyckeln saknar åtkomst till den begärda resursen.
404 not_foundBemanningsuppdraget finns inte.
409 idempotency_conflictIdempotency-Key återanvänd med annan body.
429 rate_limitedFör många anrop. Backa och försök igen.
500 internal_errorNågot oväntat. Vi loggar och utreder.

Rate limits

Standardgräns: 60 anrop per minut per API-nyckel. Webhook-callbacks räknas inte. Vid 429 returneras Retry-After i sekunder.

Varje svar inkluderar headers X-RateLimit-Limit, X-RateLimit-Remaining och X-RateLimit-Reset (unix-sekund då bucket-fönstret nollställs). Använd X-Request-Id i felrapportering — vi ekkoar samma värde i svarsheadern.

Behöver ni högre gräns? Se Enterprise-paketeringen.