Skip to content
ExploreFor youFor businessHow it worksPricing

IDOFO API

Put an AI front desk inside the software your customers already use.

IDOFO is not only an app. An EHR, a CRM, a salon or field-service platform can call the same services the IDOFO web app calls: check real availability, book, capture leads, take messages, read call summaries. Your customers keep the system they know.

Quick start

Three calls: find out who you are, ask for real availability, book it. The key decides the account — there is no tenant parameter anywhere in the API.

curl
# 1. Check the key and see what it can do
curl https://localhost:3000/api/v1/me \
  -H "Authorization: Bearer idofo_live_ak_xxx.yyy"

# 2. Ask for real open times on a specific day
curl "http://localhost:3000/api/v1/availability?date=2026-03-16" \
  -H "Authorization: Bearer idofo_live_ak_xxx.yyy"

# 3. Book one of the times that came back
curl -X POST http://localhost:3000/api/v1/appointments \
  -H "Authorization: Bearer idofo_live_ak_xxx.yyy" \
  -H "Content-Type: application/json" \
  -d '{"startsAt":"2026-03-16T14:00:00.000Z","durationMinutes":30,"title":"Consultation"}'
TypeScript
const IDOFO_KEY = process.env.IDOFO_API_KEY!;
const base = 'http://localhost:3000/api/v1';

async function idofo<T>(path: string, init: RequestInit = {}): Promise<T> {
  const res = await fetch(base + path, {
    ...init,
    headers: {
      Authorization: `Bearer ${IDOFO_KEY}`,
      'Content-Type': 'application/json',
      ...init.headers,
    },
  });
  if (!res.ok) {
    const { error } = await res.json();
    throw new Error(`${error.code}: ${error.message}`);
  }
  return res.json() as Promise<T>;
}

const { data: slots } = await idofo<{ data: { startsAt: string; label: string }[] }>(
  '/availability?date=2026-03-16',
);

if (slots.length) {
  await idofo('/appointments', {
    method: 'POST',
    body: JSON.stringify({ startsAt: slots[0].startsAt, durationMinutes: 30, title: 'Consultation' }),
  });
}

Authentication

Send the key as a bearer token: Authorization: Bearer idofo_live_<id>.<secret>. The header x-idofo-key works too, for systems that reserve the Authorization header.

  • A key belongs to exactly one account and cannot be pointed at another.
  • Only a SHA-256 hash of the secret is stored — IDOFO cannot show you the key again.
  • Keys carry scopes; give each integration the least it needs.
  • Each key has a per-minute rate limit (120 by default). Exceeding it returns 429.
  • Rotating a key issues a replacement and revokes the old one in a single step.
A malformed token, an unknown key, a wrong secret and a revoked key all return the same 401 with the same message. That is deliberate: the endpoint cannot be used to discover which keys exist.

Endpoints

All paths are under /api/v1. Every response is JSON.

MethodPathScope
GET/meassistants.read
GET/assistantsassistants.read
PATCH/assistants/:idassistants.write
GET/contactscontacts.read
POST/contactscontacts.write
GET/contacts/:idcontacts.read
PATCH/contacts/:idcontacts.write
GET/availability?date=YYYY-MM-DDappointments.read
GET/appointmentsappointments.read
POST/appointmentsappointments.write
GET/appointments/:idappointments.read
POST/appointments/:id/rescheduleappointments.write
POST/appointments/:id/cancelappointments.write
GET/taskstasks.read
POST/taskstasks.write
PATCH/tasks/:idtasks.write
GET/messagesmessages.read
POST/messagesmessages.write
GET/leadsleads.read
POST/leadsleads.write
PATCH/leads/:idleads.write
GET/callscalls.read
GET/calls/:idcalls.read
GET/calls/:id/transcripttranscripts.read
GET/businessbusiness.read
PATCH/businessbusiness.write
GET/servicesbusiness.read
POST/servicesbusiness.write
GET/knowledgeknowledge.read
POST/knowledgeknowledge.write
POST/webhookswebhooks.write
POST/assistant/conversationsassistants.write
POST/assistant/conversations/:id/turnsassistants.write
POST/assistant/conversations/:id/endassistants.write
GET/account/exportaccount.export

Booking flow

Never construct a time yourself. Ask /availability, offer what it returns, then book one of those instants. The booking endpoint re-checks the slot inside a transaction, so two integrations racing for the same time produce one booking and one 409 conflict — never a double booking.

// Availability is computed from opening hours, staff schedules,
// existing appointments, blocked time and holiday closures.
GET /api/v1/availability?date=2026-03-16&serviceId=svc_123

{
  "date": "2026-03-16",
  "timezone": "America/Chicago",
  "data": [
    { "startsAt": "2026-03-16T13:00:00.000Z", "endsAt": "2026-03-16T14:00:00.000Z", "label": "8:00 AM", "staffId": null },
    { "startsAt": "2026-03-16T15:30:00.000Z", "endsAt": "2026-03-16T16:30:00.000Z", "label": "10:30 AM", "staffId": null }
  ]
}

Talking to the assistant

You can drive a conversation from your own product — a chat widget, an inbound message, an IVR — and get the same behaviour a phone call gets, including real bookings and message taking.

POST /api/v1/assistant/conversations
{ "callerName": "Helen Marsh", "callerNumber": "+15125551201" }
-> { "callId": "cal_...", "greeting": "Thanks for calling ...", "assistant": "Ava" }

POST /api/v1/assistant/conversations/cal_.../turns
{ "message": "How much is drain cleaning, and can someone come Friday?" }
-> {
     "reply": "Drain cleaning is $149 and takes about 60 minutes. On Friday I have 8:00 AM, 10:30 AM and 1:00 PM open.",
     "toolsUsed": ["get_prices", "get_available_appointments"],
     "transferred": false,
     "needsAttention": false
   }

POST /api/v1/assistant/conversations/cal_.../end
-> { "summary": "...", "outcome": "appointment_booked", "durationSeconds": 92 }

toolsUsed tells you exactly which backend tools produced the answer. Every fact in reply came from one of those results, not from a model's memory.

Webhooks

Register an endpoint and IDOFO posts signed events to it.

EventMeaning
call.startedA call reached the assistant and a session was created.
call.completedA call ended. Payload includes summary, outcome and any records created.
appointment.createdAn appointment was booked (by a person, the assistant or the API).
appointment.rescheduledAn existing appointment moved to a new time.
appointment.canceledAn appointment was canceled.
appointment.completedSomebody recorded that an appointment went ahead.
appointment.no_showSomebody recorded that the customer did not turn up.
lead.createdA new lead was captured.
lead.updatedA lead changed status or details.
message.createdThe assistant took a message for a human.
task.createdA task was created.
task.completedA task was marked done.
contact.createdA new contact record was created.
needs_attention.createdSomething needs a human: unanswered question, failed booking, upset caller, and similar.
assistant.updatedAssistant configuration changed.
phone.number_claimedA phone number was connected to this account.
phone.went_liveIDOFO started answering real calls for this account.
calendar.connectedAn outside calendar account was connected.
calendar.disconnectedAn outside calendar account was disconnected.
Verifying the idofo-signature header
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verify(secret: string, header: string, rawBody: string): boolean {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  if (Math.abs(Date.now() - Number(parts.t)) > 300_000) return false;   // reject replays
  const expected = createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
  return timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

Retries back off at 30s, 2m, 10m, 1h and 6h, then the delivery is marked failed and kept for inspection. Payloads carry identifiers and counts, never message bodies or transcripts — fetch those through the API with your key so the permission check stays on our side.

Errors

{ "error": { "code": "not_found", "message": "Appointment not found." } }
StatusCodeWhen
400invalid_requestA field is missing or malformed.
401unauthenticatedMissing, unknown, wrong or revoked key.
402entitlement_requiredThe account’s plan does not include this feature.
402limit_exceededA plan limit has been reached.
403forbiddenThe key lacks the required scope.
404not_foundNo such record in this account.
409conflictThe time was taken while you were booking it.
429rate_limitedToo many requests for this key this minute.
503provider_unavailableAn outside provider is unavailable.

Permissions and isolation

Scopes map onto the same permissions the web app uses. The full list:

assistants.readassistants.writecontacts.readcontacts.writeappointments.readappointments.writecalls.readtranscripts.readtasks.readtasks.writemessages.readmessages.writeleads.readleads.writeknowledge.readknowledge.writebusiness.readbusiness.writewebhooks.writeaccount.export

Isolation is structural, not a filter applied at the end. Every request resolves the key to exactly one account, then builds a tenant-bound data handle; there is no code path that reads across accounts. A record id from another account returns the same 404 as an id that never existed, so ids cannot be probed.

This is verified by an automated suite that actively tries to cross the boundary — swapped ids, forged tenant fields, one account's key aimed at another's data. Results are in SECURITY_TEST_REPORT.md.

Sandbox mode

Create a key in test mode (idofo_test_…) for development. It behaves identically, so you can build against it, and the beta itself runs entirely on local mock providers: no carrier, no model vendor and no card is needed to integrate end to end.

Embedding IDOFO

Three levels, in rising order of effort:

  • API only. Your product keeps its own interface and calls IDOFO for availability, booking, messages and call data. Nothing of ours appears in your UI.
  • Embedded views. Drop IDOFO's inbox, call history or scheduling into an iframe or a panel inside your app.
  • White label. Your brand, your domain, IDOFO underneath. Available as an add-on.
The foundation for embedded views and white label exists (tenant-scoped keys, per-account branding fields, scoped read endpoints), but the embeddable components themselves are not built yet. Use the API today.

Example: an EHR or SaaS integration

Say you run scheduling software for clinics or a field-service platform for contractors. Your customers miss calls, and building a voice product is not your business. A typical integration:

  • Create one IDOFO account per customer of yours, and one API key per account.
  • Push their services, prices, hours and staff into IDOFO with PATCH /business, POST /services, POST /knowledge — so the assistant answers with facts from your system.
  • Register a webhook for appointment.created, lead.created and needs_attention.created.
  • When the webhook fires, fetch the detail with that customer's key and write it into your own tables.
  • Show call summaries in your own interface using GET /calls.
Sync one customer's configuration into IDOFO
await idofo('/business', {
  method: 'PATCH',
  body: JSON.stringify({
    legalName: clinic.name,
    category: 'Medical office',
    cancellationPolicy: clinic.policies.cancellation,
    afterHoursRule: 'Take a message. Escalate anything describing chest pain or bleeding.',
  }),
});

for (const service of clinic.services) {
  await idofo('/services', {
    method: 'POST',
    body: JSON.stringify({
      name: service.name,
      price: service.priceUsd,          // omit to have the assistant say "quoted individually"
      durationMinutes: service.minutes,
    }),
  });
}

await idofo('/webhooks', {
  method: 'POST',
  body: JSON.stringify({
    url: 'https://your-app.example.com/idofo/events',
    events: ['appointment.created', 'lead.created', 'needs_attention.created'],
  }),
});
If you work with health, legal or financial customers: IDOFO is built with the technical foundations that work needs — tenant isolation, audit trails, least-privilege access, configurable retention. That is not the same as a compliance claim. Contracts, BAAs, vendor review and hosting decisions are still required, and IDOFO does not claim HIPAA compliance today.
Developer centre · IDOFO