Inspection Support Network

Developer documentation / Guides / Schedule an order

How to schedule an order

Eight calls against the ISN API, two of them writes. Find the client, find an inspector's open time, create the order as a draft, read it back, then schedule it. Along the way: the three things the API will not check for you.

https://{host}/{companyKey}/rest

Every request is HTTP Basic: the user's access key as the username and secret access key as the password, generated in ISN under Settings → My Access Keys. Keys do not expire and carry that user's full privileges, so the user who owns them needs the CREATE_INSPECTIONS privilege. Base URLs and company keys are explained on the home page.

Before you start

Read status, not the HTTP code
Most failures come back as 200 with {"status":"error","message":"..."}, so check the body on every call. The HTTP code still matters when it is not 200: 401 is bad credentials, 403 a missing privilege, 405 a wrong path. An unknown endpoint is a 405, not a 404.
JSON needs the content type
POST reads a JSON body only when Content-Type starts with application/json. Otherwise it reads form fields, and a JSON body arrives as nothing at all, with a 200. Every GET takes a query string.
Times are on the quarter hour
Appointment times land on :00, :15, :30 or :45, as YYYY-MM-DD HH:MM:SS. Write them in the company's local time with no offset: the value is stored as written, not converted.
Nothing is reserved
Creating an order does not consult the calendar. If two systems book the same inspector at the same time, both succeed. An existing order, draft included, does hide its inspector's window from later availability calls, so creating the draft promptly narrows the race; nothing closes it.
Offices
A company with one office can skip this. A company with several has two defaults in play. Lookups (packages, services, clients, inspectors, open time) run against the office in ?office={officeUUID}, or the company's primary office. The order itself belongs to the office in officeuuid in its payload, or the key owner's office. The services on an order are checked against the lookup office, so when the two differ, services are silently dropped. The rule: put the same UUID in ?office= on every call in the flow, POST /order included, and in officeuuid on the order. An unknown office UUID is not an error; it quietly becomes the primary office. GET /offices lists them (owner privilege).

The sequence

Examples use the domain 4isn.com and the company key yourcompany, with the keys in $ACCESS_KEY and $SECRET_ACCESS_KEY. Identifiers are UUIDs and are shortened here.

  1. Check whose key you hold Read

    The order will be created as this user, in this user's office, with this user's privileges. If it is not who you expect, stop here.

    Request

    curl -u "$ACCESS_KEY:$SECRET_ACCESS_KEY" \
      https://4isn.com/yourcompany/rest/me

    Response

    { "status": "ok", "me": { "id": "9c8b…", "displayname": "Sam Okafor", "inspector": "Yes", … }, "message": "" }

    Reference: GET /me

  2. Load the catalogue Read

    An order names an order type and a list of services. Both are the company's own records, so read them once and cache them. You need one order type id and the id of each service the customer is buying.

    Request

    curl -u "$ACCESS_KEY:$SECRET_ACCESS_KEY" \
      https://4isn.com/yourcompany/rest/ordertypes
    
    curl -u "$ACCESS_KEY:$SECRET_ACCESS_KEY" \
      "https://4isn.com/yourcompany/rest/services?withAssociations"

    Response (services)

    { "status": "ok", "message": "", "services": [
      { "id": "5e4d…", "name": "Residential Inspection", "inspectiontypeid": "6f1c…",
        "fees": [ { "id": "b21a…", "name": "Residential Inspection", "amount": 450 } ], … },
      { "id": "1a2b…", "name": "Radon Test", … }
    ] }

    withAssociations adds each service's default fees and the inspectors qualified to perform it. The fee is a default, not a quote: the price on the order depends on the property, so use it for display and read the priced order back in step 7. The list includes services the company hides from its own online scheduler, marked "visible": "No"; filter them out of a customer-facing form. ancillary marks add-ons. Refresh the cache with ?after=<UTC datetime> to fetch only what changed.

    If the company sells packages, read those too. A package is a named bundle of services, and all it carries is their ids.

    Request

    curl -u "$ACCESS_KEY:$SECRET_ACCESS_KEY" \
      https://4isn.com/yourcompany/rest/packages

    Response

    { "status": "ok", "message": "", "packages": [
      { "id": "c4a7…", "name": "Buyer's Complete", "description": "…",
        "active": true, "show": true, "sequence": 1,
        "services": [ "5e4d…", "1a2b…" ], … }
    ] }
    A package is a label, not a shortcut. It has no price, and no other endpoint expands it for you: /availableslots does not accept one, and the order does not add a package's services when you name it. Everywhere below that asks for services, send the package's services plus any add-ons the customer picked, de-duplicated. To show a package price, add up its services' default fees, or price the real property with POST /calculateservicequote. Package discounts and modifiers set up for ISN's own online scheduler are not applied through the API.

    The list includes inactive and deleted packages. Keep only those with active and show both true. There is no single-package endpoint; read the list and cache it. Standalone fees live at /fees.

    Reference: GET /ordertypes · GET /services · GET /packages

  3. Find the client, or create them Write

    Look the client up by email first. A miss is a 200 with status "error" and an empty client, not an HTTP error.

    Request

    curl -u "$ACCESS_KEY:$SECRET_ACCESS_KEY" \
      https://4isn.com/yourcompany/rest/client/lookup/jamie%40example.com

    Response on a miss

    { "status": "error", "client": "",
      "message": "could not locate a client with the provided email: jamie@example.com" }

    On a miss, create the client and keep the id from the response. Creating does not de-duplicate, which is why the lookup comes first.

    Request

    curl -u "$ACCESS_KEY:$SECRET_ACCESS_KEY" \
      -H "Content-Type: application/json" \
      -d '{ "firstname": "Jamie", "lastname": "Rivera",
            "emailaddress": "jamie@example.com", "mobilephone": "208-555-0142" }' \
      https://4isn.com/yourcompany/rest/client

    Response

    { "status": "ok", "id": "0b7e…", "modified": "2026-09-22T20:05:11+00:00", "message": "" }

    The order call also accepts an inline client: { name, email, mobile } that finds or creates by display name. Prefer the explicit lookup: you get the id back, you control the match, and a renamed client does not become a duplicate.

    Reference: GET /client/lookup · POST /client

  4. Agents, if you have them Read

    Buyer's and seller's agents go on the order as ids only. Look each one up by email the same way; the miss message is could not locate an agent with the provided email.

    Request

    curl -u "$ACCESS_KEY:$SECRET_ACCESS_KEY" \
      https://4isn.com/yourcompany/rest/agent/lookup/a.chen%40example.com

    An agent id the company does not have is silently dropped from the order rather than rejected, so on a miss leave the field off instead of guessing. Creating agents needs the owner privilege, which an integration key often lacks; let the company add the agent in ISN.

    Reference: GET /agent/lookup

  5. Find open time Read

    Ask for the inspectors who can do these services in this ZIP over the next few days. Inspectors are filtered two ways: by where they work (the ZIP) and by what they are qualified to do (every service you list). Send services[] with the brackets; a bare services= is ignored. With a package, list its services and the add-ons, not the package id: there is no packages parameter.

    Request

    curl -u "$ACCESS_KEY:$SECRET_ACCESS_KEY" -G \
      --data-urlencode "zip=83702" \
      --data-urlencode "services[]=5e4d…" \
      --data-urlencode "services[]=1a2b…" \
      --data-urlencode "daysahead=7" \
      https://4isn.com/yourcompany/rest/availableslots

    Response

    { "status": "ok", "message": "", "count": 2, "zip": "83702", "daysahead": 7, "offset": 0,
      "services": [ "5e4d…", "1a2b…" ],
      "slots": [
        { "userid": "9c8b…", "start": "10/6/2026 08:00", "end": "10/6/2026 17:00",
          "userdisplay": "Sam Okafor", "userfirst": "Sam", "userlast": "Okafor" },
        { "userid": "9c8b…", "start": "10/7/2026 08:00", "end": "10/7/2026 12:00", … }
      ] }
    Each slot is an inspector's whole working window for that day, not an appointment. A window is listed only when nothing overlaps it: no blocked time, no calendar event, no order, drafts included. A window with one booking in the middle disappears entirely; you never get the fragments either side. You choose a start inside a listed window and send the length as duration. A three-hour job that starts at 16:00 in a window that ends at 17:00 will be accepted.

    Other things to know: dates are M/D/YYYY HH:MM, not ISO. Only inspectors who can perform every listed service appear. The ZIP filter applies only when the company has service areas set up; otherwise every inspector is returned. Same-day booking and the last bookable hour are company settings, so an empty list for today can be policy rather than a full calendar. Leave services[] off and you get everyone in the ZIP, qualified or not. offset=N starts N days from today; inspector=<userUUID> narrows to one person, but an id the company does not have narrows nothing and you get everyone, so check the userid on what comes back. GET /users?availableonly lists the people who have working hours at all.

    Reference: GET /availableslots · GET /users

  6. Create the order as a draft Write

    One POST with the property, the appointment, the people and the services. Leave inprogress out. The order is saved in progress: it exists and has an order number, but it is not on the calendar, sends no automatic email or SMS, and does not fire the scheduled webhook. Nobody is told anything yet.

    Request

    curl -u "$ACCESS_KEY:$SECRET_ACCESS_KEY" \
      -H "Content-Type: application/json" \
      -d @order.json \
      https://4isn.com/yourcompany/rest/order

    order.json

    {
      "address1":        "1428 Elm Street",
      "city":            "Boise",
      "state":           "ID",
      "zip":             "83702",
      "squarefeet":      2150,
      "yearbuilt":       1998,
    
      "datetime":        "2026-10-06 09:00:00",
      "duration":        180,
      "inspector1uuid":  "9c8b…",
    
      "ordertypeuuid":   "6f1c…",
      "services":        [ "5e4d…", "1a2b…" ],
      "packages":        [ { "package": "c4a7…" } ],
    
      "clientuuid":      "0b7e…",
      "buyersagentuuid": "a3d5…",
    
      "notes": [ { "owner": "9c8b…", "text": "Gate code 4412. Dog in the yard." } ]
    }

    Response

    { "status": "ok", "id": "3f9c…", "oid": 23522, "modified": "2026-09-22T20:06:40+00:00", "message": "" }

    Keep both identifiers. id is the UUID every API call wants; oid is the order number the company's staff see in ISN.

    services is the complete list; packages is only a record. Anything not in services is not on the order and is not priced, including the services of a package you name in packages. So with a package, services is the package's services plus the add-ons, de-duplicated (a repeated id is harmless), and packages notes which bundle the customer chose. Leave packages off when there is none. A package id the company does not have is a problem: the order is still created, but the response comes back "status": "partial" and the services and fees after it were never attached. Keep the id, do not create again, and fix the order with the update call.

    Fees are priced from the catalogue for those services unless you send donotcalculateservicefees: true. To override a price, add fees: [ { "feeid": "b21a…", "value": 425 } ]; an unknown feeid fails the whole request. state takes an abbreviation or a name. inspector1uuid is the userid from the slot you chose; inspector2uuid and up add more people. In a multi-office company add officeuuid and put the same id in ?office= on the URL (see Offices above). Every other field is optional and listed in the reference.

    Reference: POST /order

  7. Read it back Read

    Confirm the appointment, inspector, client and services landed the way you sent them. An id that the company does not have was dropped without an error, and this is where you find out.

    Request

    curl -u "$ACCESS_KEY:$SECRET_ACCESS_KEY" \
      https://4isn.com/yourcompany/rest/order/3f9c…

    Response

    { "status": "ok", "message": "", "order": {
      "id": "3f9c…", "oid": 23522,
      "datetime": "2026-10-06 09:00:00", "duration": 180, "scheduleddatetime": "",
      "inspector1": "9c8b…", "client": "0b7e…", "buyersagent": "a3d5…",
      "ordertype": "6f1c…", "services": [ … ], "fees": [ … ], "totalfee": 550,
      … } }

    An empty scheduleddatetime is what a draft looks like. This is the moment to show the customer a summary, take payment, or hand off to a person. If you need to notice a double booking, repeat step 5 for this inspector and day before you go on; it is coarse, but it is the only check there is.

    Reference: GET /order/{id}

  8. Schedule it Write

    Update the order with inprogress false and a scheduled timestamp, normally now. This is the call that puts the inspection on the calendar.

    Request

    curl -u "$ACCESS_KEY:$SECRET_ACCESS_KEY" \
      -H "Content-Type: application/json" \
      -d '{ "inprogress": false, "scheduled": "2026-09-22 14:06:00" }' \
      https://4isn.com/yourcompany/rest/order/3f9c…

    Response

    { "status": "ok", "id": "3f9c…", "oid": 23522, "modified": "2026-09-22T20:07:02+00:00", "message": "" }
    From here on, people get told. The company's automatic email and SMS events for a new order become eligible, the ORDER_SCHEDULED webhook fires, and the inspection appears on the inspector's calendar and in footprints. While you are testing, put "sendemailevents": false, "sendsmsevents": false on the draft in step 6 and nothing goes out, now or later: the flags stay on the order until you set them back to true.

    Both fields, every time: on this call inprogress: false without scheduled is not refused, it is ignored, and the order stays a draft with a 200. Read it back if you need to be sure. scheduledby defaults to the user whose key you hold. Anything you left off the draft can be sent in this same call, but do not repeat services or packages that are already on it: resent services are priced again and the amounts add to the fees already there, and a resent package is recorded a second time. The calendar entry needs both an appointment datetime and an inspector; a scheduled order without them is scheduled in status only. A repeat of this call fires the scheduled webhook again, so do not retry it blindly.

    Reference: POST /order/{id}

One call instead of two

If nothing happens between creating and scheduling, add the two scheduling fields to the draft payload in step 6 and skip steps 7 and 8. The order is created already scheduled, and the notifications go out at once.

Draft, then schedule

Use when a person confirms in ISN, when you collect payment first, or when you want to read the priced order back before anyone is told. This is also how the ISN MCP server's create_order and schedule_order tools behave, so the two surfaces describe one model.

Scheduled in one call

Use when your system has already taken the booking and ISN is the system of record. One thing to know: on this path the scheduled webhook fires before the services and fees are attached, so a webhook consumer that reads the order at once can see it unpriced. Add these to step 6:

  "inprogress": false,
  "scheduled":  "2026-09-22 14:06:00"

Building a booking form

A customer-facing form usually asks for the property, an inspector, a package, add-ons and a preferred time. Each part maps onto the calls above.

Package picker
GET /packages, keeping those with active and show true. Show each package's services by matching its services ids against GET /services?withAssociations, and show a price as the sum of their default fees.
Add-on list
The same services list, without "visible": "No". A service whose id is in the chosen package's services is already included; mark it that way rather than letting it be added twice.
What gets sent
One list of service ids: the package's plus the add-ons. It goes to /availableslots as services[] and to the order as services. The package id goes only to the order, in packages.
Inspector picker
Easiest with the services chosen first: /availableslots with the ZIP and services[] returns only inspectors who cover the ZIP and can do all of it, and you group the slots by userid. If the inspector comes first, start from /availableslots with just the ZIP. Then offer only packages and add-ons whose services all list that inspector in their inspectors, and check again with inspector= and services[] once the selection is made.
Preferred date
/availableslots has no date parameter. For one day, send offset as the number of days from today and daysahead=1. An order has a start time, not a window: send a quarter-hour datetime and duration, and put "10 AM to 12 PM" in a note if the window matters.
"The inspector will confirm"
That is the draft: create it in step 6 and stop. The company sees it in ISN and schedules it, or your system does step 8 later.
Several offices
Every call above takes the same ?office=, and the order also takes officeuuid. See Offices at the top.

Errors you will meet

the datetime object returned an unround time
The appointment is not on a quarter hour. Round it and resend. A time the server cannot parse at all answers please provide a valid date/time format.
please provide a scheduled date/time format
You sent inprogress: false without scheduled on the create call. Add a timestamp, normally now. The update call does not complain about the same omission; it just leaves the order a draft.
Unable to lookup Fee
A feeid in fees is not one of the company's fees. The whole request fails; nothing was saved. Ids come from GET /fees or the fees on each service.
could not locate a client with the provided email
Not an error in the HTTP sense, just a miss. Create the client (step 3).
403 this action requires user access: CREATE_INSPECTIONS
The key's user cannot create orders. The company grants the privilege in ISN, or issues keys from a user who has it.
400 this action requires the following fields
Only a few endpoints check for required fields up front, address validation among them. The message names what is missing.
405 missing or invalid action specified
The path is wrong. An unknown endpoint answers 405 rather than 404, so a typo in an endpoint name looks like a method problem.

What comes next