Loods API v1

Docs · API v1

The model

Four calls to an AI model that runs in the EU. You send what the model may see and get structured data back. Loods keeps the token count and nothing else.

How every call works

  • Stateless. The model sees only what you send in this call. Nothing is kept for the next one.
  • guidance tells the model about your business: who you write as, and the tone you keep. It's optional on every call, up to 10,000 characters, and is added under Loods' own rules. It can't switch them off.
  • context is what the model may know about the conversation. It's passed as data, never as instructions.
  • usage comes back on every answer: the model, and the prompt and completion tokens. model is null when nothing was asked of it.
  • No AI arithmetic. Money comes back as integers per line. There's no total field anywhere: you add it up.

When the model fails, the answer is a 502 of type upstream_error. The reference lists the codes. Nothing was stored or sent, so you can try again.

Read a lead

POST /v1/interpret/lead reads the contact fields, the address of the work, a short title and a summary.

  • text: the lead as it came. It's clipped at 20,000 characters.
  • source: where it came from, in your words.
  • known: fields you already have. What you pass here beats what the model reads.

Phone numbers come back in E.164 where they can be, email addresses in lower case, and Dutch postcodes as 1234 AB.

POST/v1/interpret/lead
curl https://loods.dev/v1/interpret/lead \
  -H "Authorization: Bearer $LOODS_KEY" \
  -H "Content-Type: application/json" -d '{
    "text": "Dakkapel aan de achterzijde, circa drie meter breed. …",
    "source": "website",
    "known": { "email": "m.devries@voorbeeld.nl" }
  }'
200 OK
{
  "name": "Marieke de Vries",
  "email": "m.devries@voorbeeld.nl",
  "phone": "+31623456789",
  "street": "Oudegracht",
  "house_number": "212",
  "postcode": "3511 NP",
  "city": "Utrecht",
  "title": "Dakkapel plaatsen",
  "summary": "Dakkapel aan de achterzijde van circa drie meter breed; de klant wil binnen twee weken starten.",
  "usage": { "model": "…", "prompt_tokens": 932, "completion_tokens": 137 }
}

Read a message

POST /v1/interpret/message picks one of your intents for a message and writes a one-line summary.

  • from, subject and body: the message.
  • intents: a list of values, or an object of value to what it means.
  • strip_quoted: cut quoted history and signatures first. On by default.
  • mask: take the people out before the model reads, and put them back into the answer. Off by default. See below.

To get prices, add price_lines. Lines come back only when the intent is when_intent, which must be one of your intents. Each line has a unit, a cost_type and a vat_rate from the lists you give.

quantity_milli is in thousandths, so 2.5 m² is 2500. unit_price_cents is in cents, excluding VAT.

POST/v1/interpret/message
{
  "from": "aannemer@bouwbedrijf.nl",
  "subject": "Prijsopgave dakkapel",
  "body": "…",
  "intents": {
    "gives_price": "The contractor names a price",
    "asks_question": "The contractor asks something first",
    "declines": "The contractor won't do it"
  },
  "price_lines": {
    "when_intent": "gives_price",
    "units": ["post", "m2", "hour"],
    "cost_types": ["labour", "material"],
    "vat_rates": [21, 9]
  }
}
200 OK
{
  "intent": "gives_price",
  "summary": "De aannemer geeft een prijs voor de dakkapel.",
  "lines": [
    { "description": "Dakkapel plaatsen", "quantity_milli": 1000, "unit": "post",
      "unit_price_cents": 425050, "cost_type": "labour", "vat_rate": 21 },
    { "description": "Isolatie", "quantity_milli": 2500, "unit": "m2",
      "unit_price_cents": 2000, "cost_type": "material", "vat_rate": 9 }
  ],
  "usage": { "model": "…", "prompt_tokens": 1204, "completion_tokens": 160 }
}

Draft a reply

POST /v1/draft fills the gaps in a message you wrote. Mark each gap [1], [2] and so on in the outline, and give one instruction per gap in gaps, up to 20.

The prose around the gaps never changes. You get one fill per gap, in order, and render them back into your own template. A fill is "" when the model had nothing to add.

Add companion for a short message to go with it, such as a chat line announcing the mail. Give an instruction and max_chars, from 20 to 4096.

With no gaps and no companion, nothing is asked of the model and nothing is spent.

Send "mask": true on a draft or a message and the model never reads who it is about. Names, addresses, towns, postcodes, telephone numbers, e-mail addresses and ibans are taken out before the call and put back into the answer, so the response has the same shape either way. It is per request, so you can try it on one message or turn it on for one of your own customers. Formats and the names in a greeting or under a sign-off are found; a surname in the middle of a sentence or a company name can still be read.

POST/v1/draft
{
  "outline": "Beste Marieke,\n\n[1]\n\nMet vriendelijke groet,\nSam",
  "gaps": ["Thank her, and ask how wide the back wall is."],
  "context": { "lead": { "category": "Dakkapel plaatsen" } },
  "companion": {
    "instruction": "Tell her an email about her dormer is on its way.",
    "max_chars": 160
  }
}
200 OK
{
  "fills": ["Dank voor uw aanvraag. Om een goede prijs te kunnen geven, hoor ik graag hoe breed de achtergevel is."],
  "companion": "Hoi Marieke, Sam van Bouwbedrijf Voorbeeld heeft u zojuist een e-mail gestuurd over uw dakkapel.",
  "usage": { "model": "…", "prompt_tokens": 1450, "completion_tokens": 96 }
}

Anything else

POST /v1/extract answers your own instructions about an input, in your own JSON Schema.

The schema is for an object, and the model is held to it strictly. So list every property in required and set additionalProperties: false.

The answer is in data.

Send "mask": true and the people are taken out of instructions, input and context and put back into data, as on a draft. Here it is off unless you send it, because your own schema may hold something a mask would spoil. Nothing masks a picture.

POST/v1/extract
{
  "instructions": "Does the customer ask for a visit, and on which days can they?",
  "input": "…",
  "schema": {
    "type": "object",
    "required": ["wants_visit", "days"],
    "additionalProperties": false,
    "properties": {
      "wants_visit": { "type": "boolean" },
      "days": { "type": "array", "items": { "type": "string" } }
    }
  }
}
200 OK
{
  "data": { "wants_visit": true, "days": ["dinsdag", "donderdag"] },
  "usage": { "model": "…", "prompt_tokens": 388, "completion_tokens": 21 }
}