Lawfficient API
Consultations

Book a consultation

POST /api/consultations: book a consultation into an attorney's free slot.

POST /api/consultations

Books a consultation (scope consultations:write). The payload is validated, the requested time is checked against the attorney's office hours and time off for that day, and the database guarantees nobody else holds that time. The booked consultation is returned in full with status scheduled, and a consultation.booked webhook fires. If the firm maps "consultation booked" (or, for a booking sent with paid: true, "consultation paid") to a pipeline stage, the linked lead also moves to it and a lead.status_changed webhook fires. It doesn't move (and no webhook fires) when the lead is archived, in a final stage or already in that stage, or when the stage is retired.

Request body

JSON. lead_id, attorney_id, type, start_at, and duration_min are required. The API books a real slot, so both the lead and the attorney are mandatory: an attorney is needed to validate office hours.

FieldTypeNotes
lead_idstringRequired. The lead (UUID) this consultation is for.
attorney_idstringRequired. A schedulable, active attorney (UUID) in your firm.
typestringRequired. The consultation type (free text, e.g. Initial consultation).
start_atstringRequired. ISO 8601 start instant, interpreted as UTC.
duration_minnumberRequired. Length in minutes, 5 to 1440.
time_zonestringOptional IANA zone the consultation is shown in. Defaults to America/New_York.
paidbooleanOptional: book it as already paid (for example, paid outside the app). Card payments are taken in the app. Defaults to false.
amountnumber | nullOptional fee in dollars; nonnegative, or null. 0 books a free consultation, which holds its time. Left out or null, no fee is recorded and the booking is unpaid: it shares its time until it's paid.
dataobjectOptional practice specific fields.

start_at is a UTC instant, not a wall clock time: the API is machine to machine, so there's no time zone conversion on the way in. time_zone only sets how the consultation is displayed in the app. Validation of office hours and time off compares the resulting local time against the attorney's schedule.

Unpaid bookings share a time

A consultation holds its time on the attorney's calendar once it's paid, or when its fee is zero (amount of 0). An unpaid booking doesn't, and that includes one sent without an amount (or with null): no fee is recorded, so it's unpaid. Several unpaid bookings can share a time (the office books them in pencil), and the first one paid takes it. Only consultations that end after now count: an overlap in the past is history. So:

  • Booking an unpaid consultation into a time other unpaid bookings already share succeeds.
  • Booking anything into a time a paid or free consultation already holds is a 409 slot_unavailable, and so is a booking sent with paid: true (or amount: 0) over another booking that holds the time, or over one being paid for by a live card right now (a test charge never blocks it).
  • When a booking starts holding a time over unpaid ones, those stay booked and are flagged "Needs a new time" in the app for staff to move; they're never canceled automatically.

Idempotency

Send an Idempotency-Key header to make a booking safe to retry: a repeat with the same key returns the original consultation instead of booking a second. Use a fresh unique value (e.g. a UUID) per logical booking.

Idempotency-Key: 9d2b7c1e-5a4b-4c3d-8e2f-1a2b3c4d5e6f

Example

curl https://app.lawfficient.com/api/consultations \
  -H "Authorization: Bearer $LAWFFICIENT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 9d2b7c1e-5a4b-4c3d-8e2f-1a2b3c4d5e6f" \
  -d '{
    "lead_id": "3f8c1e2a-1b2c-4d5e-8f90-abcdef012345",
    "attorney_id": "a1b2c3d4-0000-4000-8000-000000000000",
    "type": "Initial consultation",
    "start_at": "2026-07-02T15:00:00.000Z",
    "duration_min": 30,
    "time_zone": "America/New_York"
  }'
201 Created
{
  "id": "7c1e2a3f-1b2c-4d5e-8f90-abcdef012345",
  "lead_id": "3f8c1e2a-1b2c-4d5e-8f90-abcdef012345",
  "attorney_id": "a1b2c3d4-0000-4000-8000-000000000000",
  "type": "Initial consultation",
  "status": "scheduled",
  "start_at": "2026-07-02T15:00:00.000Z",
  "duration_min": 30,
  "time_zone": "America/New_York",
  "paid": false,
  "amount": 150,
  "outcome": null,
  "archived": false,
  "created_at": "2026-06-28T10:00:00.000Z",
  "data": {}
}

Errors

Errors use the error envelope:

codeStatusWhen
invalid_request422A missing/invalid field, a lead_id/attorney_id that isn't a UUID, or a lead/attorney that isn't a bookable member of your firm. Also a lead that is missing a contact field your firm requires at booking (the message names the field's key); fill it in on the lead (in the app, or through data with Update a lead), then book again.
outside_office_hours422The time falls outside the attorney's office hours.
attorney_unavailable422The attorney has time off (or a holiday) on that date.
slot_unavailable409Another consultation already holds that time on the attorney's calendar (it's paid, or its amount is 0), or, for a paid or free booking, another consultation at that time is being paid for by a live card right now (test charges never block it). The message says which.
insufficient_scope403The key lacks consultations:write.

Rejections for office hours or time off are a 422 (the request is well formed but the slot isn't bookable), while a time another consultation holds is a 409. Branch on the code, not the status, to tell a slot outside office hours from a held time.

On this page