REST API v1

Formisoft API

Build integrations with the Formisoft platform. Create and manage forms, patients, appointments, submissions, and webhooks programmatically using a simple REST API.

Base URLhttps://formisoft.com/api
Get API Key

Capabilities

Create & manage forms
CRUD patients
Book appointments
Review submissions
Configure webhooks
View analytics

Authentication

All API requests require authentication via an API key. Keys are scoped to an organization and carry full access to that organization's data.

Create an API Key

  1. Go to Settings → API Keys in your dashboard
  2. Click "Create API Key" and give it a name
  3. Copy the key immediately — it is only shown once

Use Your Key

Include your key in the Authorization header:

Authorization: Bearer fsk_your_api_key_here

Keep your API keys secret. Do not expose them in client-side code, public repositories, or browser requests. API keys have full access to your organization's data including PHI.

Quick Start

List forms

curl https://formisoft.com/api/forms \
  -H "Authorization: Bearer fsk_your_api_key_here"

Create a patient

curl -X POST https://formisoft.com/api/patients \
  -H "Authorization: Bearer fsk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "firstName": "Jane",
    "lastName": "Doe",
    "email": "jane@example.com",
    "dateOfBirth": "1990-01-15"
  }'

Book an appointment

curl -X POST https://formisoft.com/api/appointments \
  -H "Authorization: Bearer fsk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "patientId": "clx...",
    "scheduledAt": "2025-03-01T14:00:00Z",
    "duration": 30,
    "type": "initial_consultation"
  }'

Forms

GET/api/forms

List all forms for your organization. Returns forms sorted by last updated.

Response

[
  {
    "id": "clx...",
    "name": "New Patient Intake",
    "description": "Standard intake form",
    "category": "intake",
    "status": "published",
    "pages": [...],
    "settings": {...},
    "branding": {...},
    "_count": { "submissions": 42 },
    "createdAt": "2025-01-15T10:00:00.000Z",
    "updatedAt": "2025-01-20T14:30:00.000Z"
  }
]
POST/api/forms

Create a new form.

Request Body

{
  "name": "New Patient Intake",        // required
  "description": "Standard intake",    // optional
  "category": "intake",                // optional, default: "intake"
  "pages": [...],                      // optional, default: empty page
  "settings": {},                      // optional
  "branding": {}                       // optional
}

Response

{
  "id": "clx...",
  "name": "New Patient Intake",
  "status": "draft",
  "createdAt": "2025-01-15T10:00:00.000Z",
  ...
}

Submissions

GET/api/submissions

List submissions. Filter by form or status.

Query Parameters

formId    Filter by form ID (optional)
status    Filter by status: "submitted" | "reviewed" | "flagged" (optional)
limit     Max results, default 100, max 1000, or "all"

Response

[
  {
    "id": "clx...",
    "formId": "clx...",
    "status": "submitted",
    "data": { "field_id": "value", ... },
    "form": { "id": "clx...", "name": "Intake Form" },
    "patient": { "id": "clx...", "firstName": "Jane", "lastName": "Doe" },
    "reviewer": null,
    "createdAt": "2025-01-20T09:15:00.000Z"
  }
]
PATCH/api/submissions

Update a submission's review status.

Request Body

{
  "submissionId": "clx...",             // required
  "status": "reviewed",                 // "submitted" | "reviewed" | "flagged"
  "reviewNotes": "Looks good"           // optional
}

Response

{
  "id": "clx...",
  "status": "reviewed",
  "reviewedAt": "2025-01-20T10:00:00.000Z",
  "reviewer": { "id": "clx...", "name": "Dr. Smith" },
  ...
}
PATCH/api/submissions/:id

Update a submission by ID. Same behavior as the batch endpoint above.

Request Body

{
  "status": "reviewed",                 // "submitted" | "reviewed" | "flagged"
  "reviewNotes": "Approved"             // optional
}

Response

{
  "id": "clx...",
  "status": "reviewed",
  "reviewedAt": "2025-01-20T10:00:00.000Z",
  ...
}
DELETE/api/submissions/:id

Delete a submission permanently.

Response

{ "success": true }

Patients

GET/api/patients

List patients. Supports search across name, email, and phone.

Query Parameters

search    Search term for name, email, or phone (optional)
status    Filter by status (optional)

Response

[
  {
    "id": "clx...",
    "patientId": "PAT-000001",
    "firstName": "Jane",
    "lastName": "Doe",
    "email": "jane@example.com",
    "phone": "+15551234567",
    "dateOfBirth": "1990-01-15T00:00:00.000Z",
    "gender": "female",
    "status": "active",
    "_count": { "appointments": 3, "submissions": 5 },
    "createdAt": "2025-01-10T08:00:00.000Z"
  }
]
POST/api/patients

Create a new patient record.

Request Body

{
  "firstName": "Jane",                  // required
  "lastName": "Doe",                    // required
  "email": "jane@example.com",          // optional
  "phone": "+15551234567",              // optional
  "dateOfBirth": "1990-01-15",          // optional, ISO date
  "gender": "female",                   // optional
  "address": { ... },                   // optional, JSON object
  "insuranceCarrier": "Aetna",          // optional
  "insurancePolicyNumber": "ABC123",    // optional
  "insuranceGroupNumber": "GRP456",     // optional
  "insuranceSubscriber": "Jane Doe",    // optional
  "emergencyContactName": "John Doe",   // optional
  "emergencyContactPhone": "+15559876543", // optional
  "emergencyContactRelationship": "Spouse" // optional
}

Response

{
  "id": "clx...",
  "patientId": "PAT-000042",
  "firstName": "Jane",
  "lastName": "Doe",
  "status": "active",
  "createdAt": "2025-01-15T10:00:00.000Z",
  ...
}
GET/api/patients/:id

Get a single patient with medical data, recent appointments, submissions, and documents.

Response

{
  "id": "clx...",
  "firstName": "Jane",
  "lastName": "Doe",
  "allergies": [...],
  "medications": [...],
  "conditions": [...],
  "appointments": [...],
  "submissions": [...],
  "documents": [...],
  "payments": [...],
  "_count": { "appointments": 3, "submissions": 5, "documents": 2 },
  ...
}
PUT/api/patients/:id

Update a patient record. Only include fields you want to change.

Request Body

{
  "firstName": "Janet",                 // any patient field
  "email": "janet@example.com",
  "status": "inactive",
  "insuranceCarrier": "BlueCross"
  // ... any field from the create endpoint
}

Response

{
  "id": "clx...",
  "firstName": "Janet",
  "email": "janet@example.com",
  "status": "inactive",
  ...
}
DELETE/api/patients/:id

Delete a patient record permanently.

Response

{ "success": true }

Appointments

GET/api/appointments

List appointments for your organization, sorted by most recent.

Response

[
  {
    "id": "clx...",
    "patientId": "clx...",
    "providerId": "clx...",
    "scheduledAt": "2025-02-01T14:00:00.000Z",
    "duration": 30,
    "type": "initial_consultation",
    "status": "scheduled",
    "patient": {
      "id": "clx...",
      "firstName": "Jane",
      "lastName": "Doe",
      "email": "jane@example.com"
    },
    "provider": { "user": { "name": "Dr. Smith" } },
    "forms": [{ "form": { "id": "clx...", "name": "Intake Form" } }]
  }
]
POST/api/appointments

Create an appointment. Checks for scheduling conflicts automatically.

Request Body

{
  "patientId": "clx...",                // required
  "scheduledAt": "2025-02-01T14:00:00Z", // required, ISO datetime
  "providerId": "clx...",              // optional
  "duration": 30,                       // optional, minutes (default: 30)
  "type": "initial_consultation",       // optional
  "notes": "First visit",              // optional
  "formIds": ["clx..."],               // optional, attach intake forms
  "autoSendIntake": true                // optional, email forms to patient
}

Response

{
  "id": "clx...",
  "status": "scheduled",
  "scheduledAt": "2025-02-01T14:00:00.000Z",
  "duration": 30,
  "patient": { "id": "clx...", "firstName": "Jane", "lastName": "Doe" },
  ...
}
GET/api/appointments/:id

Get a single appointment with patient, provider, and form details.

Response

{
  "id": "clx...",
  "scheduledAt": "2025-02-01T14:00:00.000Z",
  "duration": 30,
  "status": "scheduled",
  "patient": { ... },
  "provider": { "user": { "name": "Dr. Smith" } },
  "forms": [...],
  "organization": { "name": "Acme Clinic", "timezone": "America/New_York" }
}
PUT/api/appointments/:id

Update an appointment. Supports rescheduling (with conflict detection) and status transitions.

Request Body

{
  "scheduledAt": "2025-02-02T10:00:00Z", // reschedule
  "duration": 45,                         // update duration
  "status": "confirmed",                  // status transition
  "type": "follow_up",                    // update type
  "notes": "Rescheduled",
  "cancelReason": "Patient unavailable"   // when status = "cancelled"
}

Response

{
  "id": "clx...",
  "status": "confirmed",
  "scheduledAt": "2025-02-02T10:00:00.000Z",
  ...
}
DELETE/api/appointments/:id

Delete an appointment permanently.

Response

{ "success": true }

Status Transitions

Appointments follow a state machine. Invalid transitions return 422.

scheduled   → confirmed, checked_in, cancelled, completed, no_show
confirmed   → scheduled, checked_in, cancelled, completed, no_show
checked_in  → in_progress, cancelled, completed, no_show
in_progress → completed, cancelled
cancelled   → scheduled
no_show     → scheduled
completed   → (terminal)

Webhooks

GET/api/webhooks

List all webhook configurations for your organization.

Response

[
  {
    "id": "clx...",
    "url": "https://example.com/webhook",
    "events": ["submission.created", "patient.created"],
    "active": true,
    "_count": { "deliveries": 15 },
    "createdAt": "2025-01-10T08:00:00.000Z"
  }
]
POST/api/webhooks

Create a new webhook. Returns the signing secret (shown once).

Request Body

{
  "url": "https://example.com/webhook",  // required, HTTPS recommended
  "events": ["submission.created"],       // required, array of event types
  // OR for single event:
  "event": "submission.created"           // alternative to events array
}

Response

{
  "id": "clx...",
  "secret": "a1b2c3d4e5f6..."           // HMAC signing secret — shown once
}
PUT/api/webhooks/:id

Update a webhook configuration.

Request Body

{
  "url": "https://example.com/new-hook", // optional
  "events": ["submission.created", "patient.created"], // optional
  "active": false                         // optional, disable/enable
}

Response

{
  "id": "clx...",
  "url": "https://example.com/new-hook",
  "events": ["submission.created", "patient.created"],
  "active": false,
  ...
}
DELETE/api/webhooks/:id

Delete a webhook configuration and all its delivery history.

Response

{ "success": true }

Analytics

GET/api/analytics

Get form analytics data (view locations).

Query Parameters

formId    The form ID to get analytics for (required)

Response

{
  "data": [
    {
      "latitude": 40.7128,
      "longitude": -74.006,
      "city": "New York",
      "region": "NY",
      "country": "US"
    }
  ]
}

Webhook Events

When events occur in Formisoft, we send a POST request to your webhook URL with a JSON payload. Deliveries time out after 10 seconds.

Supported Events

submission.createdA new form submission is received
submission.reviewedA submission is marked as reviewed or flagged
appointment.createdA new appointment is booked
appointment.updatedAn appointment is updated, rescheduled, or cancelled
patient.createdA new patient record is created
patient.updatedA patient record is updated
form.publishedA form is published

Payload Format

Every webhook payload includes the event field plus event-specific data:

// submission.created
{
  "event": "submission.created",
  "formId": "clx...",
  "submissionId": "clx...",
  "patientId": "clx...",
  "patientName": "Jane Doe"
}

// appointment.updated
{
  "event": "appointment.updated",
  "appointmentId": "clx...",
  "status": "confirmed",
  "scheduledAt": "2025-02-01T14:00:00.000Z"
}

// patient.created
{
  "event": "patient.created",
  "patientId": "clx...",
  "firstName": "Jane",
  "lastName": "Doe",
  "email": "jane@example.com"
}

Signature Verification

Each delivery includes an X-Webhook-Signature header — an HMAC-SHA256 hex digest of the JSON body signed with your webhook secret. Verify it to ensure the request came from Formisoft:

const crypto = require("crypto");

function verifyWebhook(body, signature, secret) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(JSON.stringify(body))
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

// Usage in Express
app.post("/webhook", (req, res) => {
  const sig = req.headers["x-webhook-signature"];
  if (!verifyWebhook(req.body, sig, WEBHOOK_SECRET)) {
    return res.status(401).send("Invalid signature");
  }
  // Process event...
  res.sendStatus(200);
});

Rate Limits

API requests are rate-limited per IP address using a sliding window. If you exceed the limit, you'll receive a 429 Too Many Requests response.

Retry with exponential backoff. Most endpoints allow 60 requests per minute.

Errors

All errors return a JSON object with an error field and an appropriate HTTP status code:

{ "error": "Unauthorized" }
StatusMeaning
400Bad request — missing or invalid parameters
401Unauthorized — invalid or missing API key
403Forbidden — key does not have access to this resource
404Not found — resource does not exist
409Conflict — scheduling conflict (appointments)
422Unprocessable — invalid state transition
429Too many requests — rate limit exceeded
500Internal server error

Ready to integrate?

Create your API key in the dashboard and start building. Full access to all endpoints with every plan.