Skip to content

loan

A loan records equipment handed out to a borrower who is expected to bring it back: a drill lent to a site, a laptop issued to an employee. It holds the handover (items, condition, photos, accessories, conditions accepted, signatures, protocol PDF) and every return (what came back, in which condition, what was lost, signatures, protocol PDF).

  • Mutable: the version with the greater hlc wins. Every return appends to returns and writes a new version of the loan.
  • Ids: random UUIDs.
  • Items leave stock through issue movements and come back through receipt movements, both with loan_id. Lost items close without a movement.
  • What is still out, whether the loan is overdue and the penalty so far are derived, never stored.
  • The reference app never deletes loans.
Field Type Writer Default Meaning
number string server Loan number as printed, UE-{YYYY}-{DEV}-{SEQ:4} in the reference app (see Number ranges)
borrower_id uuid server The recipient who borrows
borrower object server Copy of the borrower at handover, the same shape as a delivery note's recipient; name is required
borrower_kind string? optional employee customer, site or employee at handover. Decides which conditions apply and whether a due date is needed. Readers treat absent or unknown values as employee.
borrower_department string? optional none Copy of the employee's department
borrower_email string? optional none Copy of the borrower's email address, where the protocols can be sent
purpose string? optional none What the equipment is for, for example a project
note string? optional none Remark printed on the handover protocol
issued_at date-time server Time of the handover
issued_by string? optional none Display name of the person handing out
due_at date-time? required for customers and sites none Last day to return on, as the start of that day in the time zone of the device that created the loan. Optional for employees.
items array server What was handed out, see below
signatures array? optional [] Signatures at handover, same shape as on delivery notes. The borrower's (recipient) is required to create a loan; the issuer's is optional.
returns array? optional [] Every return, see below
terms string? optional none The conditions exactly as the borrower accepted them, including the penalty clause. Frozen at handover.
penalty object? optional none Penalty scale agreed at handover; null when none. See below.
pdf sha256? optional none Attachment: the handover protocol
Field Type Writer Default Meaning
position integer server Line number, starting at 1. Returns refer to items by position.
article_id uuid server The article
sku string optional "" Copy of the item number at handover
name string optional "" Copy of the article name
unit string optional Stk Copy of the unit
quantity number server Quantity handed out
serials array of string? optional [] Serial numbers handed out
from_location_id uuid server Where the item left stock
location_code string optional "" Copy of the location code
replacement_value number? optional none Value per unit the borrower replaces if the item is lost or destroyed, in the inventory currency. Without it the usual replacement value of a comparable item in corresponding condition applies; see Replacement value. The reference app suggests the article's purchase_price.
condition string? optional none Condition at handover in words, for example "new" or "scratches on the lid"
photos array of sha256? optional [] Attachments: JPEG photos of the condition at handover
accessories array of string? optional [] Accessories handed out with the item. The loan stays open until each is returned or reported lost.
movement_id uuid server The issue movement that booked the item out

A return is written only after both the borrower (recipient) and the person taking the items back (issuer) have signed; until then nothing is booked.

Field Type Writer Meaning
id uuid server Identifies the return within the loan
returned_at date-time server Time of the return
received_by string? optional Display name of the person who took the items back
items array server Return lines, see below
signatures array required Signatures of recipient (the borrower) and issuer, same shape as above. The reference app does not show a loan whose return lacks this array.
pdf sha256? optional Attachment: the return protocol
Field Type Writer Default Meaning
position integer server position of the loan item this line belongs to
quantity number server Units returned or reported lost with this line. 0 when the line only hands in or writes off accessories.
serials array of string? optional [] Serial numbers returned or reported lost
condition string server ok ok and damaged: the units come back into stock with a receipt movement. lost: the units are closed without a movement.
note string? optional none For example what is damaged
photos array of sha256? optional [] JPEG photos of the condition at return
accessories array of string? optional [] Accessories of the item that came back with this line
lost_accessories array of string? optional [] Accessories of the item reported lost with this line
to_location_id uuid? optional none Where the units went back into stock; null for lost units and accessory-only lines
movement_id uuid? optional none The receipt movement; null when nothing was booked
Field Type Writer Meaning
tiers array server when penalty is present Daily rates, see below
tiers[].after_days integer, at least 0 server Late days that pass before this rate applies; 0 starts on the first late day
tiers[].per_day number, at least 0 server Amount per late calendar day in the inventory currency
cap_amount number? optional Upper limit of the total as an amount
cap_percent number? (0 to 100) optional Upper limit as a percentage of the total replacement value (sum of replacement_value times quantity over all items)

The amount for a delay is computed as described in Penalty amount. A penalty is only agreed for external borrowers with a due date; see When a penalty applies.

At handover:

  • items is not empty. Each item's issue movement exists with loan_id, the loan number in note, and the same article, location, quantity and serials.
  • Customers and sites need due_at. The reference app stores the start of the chosen day in the device's time zone.
  • signatures contains the borrower's signature (recipient); the issuer's is optional. The reference app keeps the borrower's first.
  • terms and penalty are frozen copies; they never change afterwards.
  • accessories are trimmed, without blanks or duplicates (case-insensitive).

At a return (a new version of the loan with one more entry in returns):

  • Both recipient and issuer signatures are present.
  • Every line refers to an item by position. A line has a quantity above 0, or accessories, or lost accessories.
  • For items with serial numbers, the line lists exactly quantity serials, all of them still outstanding. For items without, quantity does not exceed what is outstanding.
  • Accessories and lost accessories are among the item's outstanding accessories (case-insensitive) and appear only once.
  • A line with quantity above 0 and condition ok or damaged names to_location_id and has a receipt movement with loan_id, the loan number in note, to_location_id and the returned serials.
  • Earlier returns stay unchanged; the return is appended.

The reference app checks all of this before it writes anything, so a failing line never leaves a half-booked return.

Value Rule
Outstanding quantity, serials, accessories per item Loans
Status open, overdue, closed Per loan
Overdue days Full calendar days after the day of due_at
Accrued penalty Penalty amount while something is outstanding
Serial numbers on loan Issued serial numbers that are outstanding on an open loan show the loan number, borrower and due date
Field Rules
number required, string
borrower_id required, UUID
borrower required, object; borrower.name required, string
borrower_kind nullable, one of customer, site, employee
borrower_department, borrower_email, purpose, note, issued_by, terms nullable, string
issued_at required, date
due_at nullable, date
items required, list of objects
items.*.position required, integer
items.*.article_id, items.*.from_location_id, items.*.movement_id required, UUID
items.*.quantity required, numeric
items.*.serials nullable, list; each element a string
items.*.replacement_value nullable, numeric, at least 0
items.*.photos nullable, list; each element matching ^[0-9a-f]{64}$
items.*.accessories nullable, list; each element a string
signatures nullable, list; role nullable, one of recipient, issuer; image nullable, SHA-256
returns nullable, list
returns.*.id required, UUID
returns.*.returned_at required, date
returns.*.items required, list
returns.*.items.*.position required, integer
returns.*.items.*.quantity required, numeric
returns.*.items.*.condition required, one of ok, damaged, lost
returns.*.items.*.photos nullable, list of SHA-256
returns.*.items.*.accessories, returns.*.items.*.lost_accessories nullable, list; each element a string
returns.*.items.*.to_location_id, returns.*.items.*.movement_id nullable, UUID
returns.*.signatures nullable, list; role nullable, one of recipient, issuer; image nullable, SHA-256
returns.*.pdf, pdf nullable, SHA-256
penalty nullable, object (shape not checked on loans)

The server does not check that due_at is present for external borrowers, that the signatures are complete, or that returns match the items. Those are rules for writers.

A drill lent to a site, returned damaged a day late with one accessory missing:

{
"entity": "loan",
"id": "E2C4A6B8-1D3F-4A5C-9E7B-0F2D4B6A8C91",
"op": "upsert",
"hlc": "001792481400000-0000-3F2A9C1B",
"data": {
"number": "UE-2026-3F-0001",
"borrower_id": "5E2A8C14-9D3B-4A7F-B6C0-1E4D7F2A9B35",
"borrower": {
"kind": "site", "name": "Northring construction site", "number": "K-10042",
"street": "Northring 12", "postal_code": "10115", "city": "Berlin", "country": "Germany", "contact": "Max Mustermann"
},
"borrower_kind": "site",
"purpose": "Installation level 3",
"issued_at": "2026-10-12T11:00:00Z",
"issued_by": "Erika Mustermann",
"due_at": "2026-10-18T22:00:00Z",
"items": [
{
"position": 1,
"article_id": "4A7C2E91-0B3D-4F6A-8E15-7C9B2D4F6A80",
"sku": "200310",
"name": "Cordless drill 18 V",
"unit": "Stk",
"quantity": 1,
"serials": ["DR-1001"],
"from_location_id": "2B0E6C1A-7D44-4F1B-9C3E-5A8D2F6B1C07",
"location_code": "N-A01",
"replacement_value": 189.9,
"condition": "new",
"photos": ["c1479c9c070dfaaf003b828b11d691dc9a4d455bf6f693a44de9e1080400b162"],
"accessories": ["Charger", "Battery 4 Ah", "Case"],
"movement_id": "A3B5C7D9-2E4F-4061-8A3C-5E7F9B1D3F52"
}
],
"signatures": [
{ "role": "recipient", "name": "Max Mustermann", "signed_at": "2026-10-12T11:02:00Z", "image": "acc6bcb1913432eed9c60781607713d63c802ff23893d35b88b1903ed04c2f50" },
{ "role": "issuer", "name": "Erika Mustermann", "signed_at": "2026-10-12T11:02:30Z", "image": "557643b476f24dda4ec227e3f41c0e8070d765f37c74c3bfa0d9dcfa6720eeee" }
],
"returns": [
{
"id": "F1A3C5E7-9B2D-4F46-8A0C-2E4B6D8F0A17",
"returned_at": "2026-10-20T07:30:00Z",
"received_by": "Erika Mustermann",
"items": [
{
"position": 1,
"quantity": 1,
"serials": ["DR-1001"],
"condition": "damaged",
"note": "Chuck wobbles",
"photos": ["84358bf14527752ea15b31417c718a0e6a7a92ad904b2e1f856574695b9bb611"],
"accessories": ["Charger", "Case"],
"lost_accessories": [],
"to_location_id": "2B0E6C1A-7D44-4F1B-9C3E-5A8D2F6B1C07",
"movement_id": "C7E9A1B3-5D7F-4291-B3D5-7F9A1C3E5B64"
}
],
"signatures": [
{ "role": "recipient", "name": "Max Mustermann", "signed_at": "2026-10-20T07:31:00Z", "image": "f4ea7a3453ab1b8fdeeaf7ee8d9fd4104e43956b95855a61c1ef9f3e180d6f6d" },
{ "role": "issuer", "name": "Erika Mustermann", "signed_at": "2026-10-20T07:31:30Z", "image": "be7d9df48d637d1f1e1684b07569c572999cc50eca2862a73f7dc25798f2b082" }
],
"pdf": "e65034da1a35513b1e77bb281f4bcb7fbc39abd8712d54ddfea59aaa974a2db8"
}
],
"terms": "The borrower returns the equipment by the due date ...",
"penalty": { "tiers": [{ "after_days": 2, "per_day": 5 }, { "after_days": 14, "per_day": 10 }], "cap_amount": null, "cap_percent": 30 },
"pdf": "28b405d5ad469ff43888136a56c515bc0762856c8052bd09830433634d8997c0"
}
}

After this return the drill is back (damaged) and the loan stays open, overdue, because the "Battery 4 Ah" is still out. The loan workflow continues the example.

schemas/loan.json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Loan",
"description": "Equipment handed out to an employee, customer or site, with the conditions the borrower accepted and every return. Mutable; the version with the greater hlc wins. Items leave stock through issue movements and come back through receipt movements, both with loan_id. Lost items are closed by a return line without a movement.",
"type": "object",
"$defs": {
"uuid": {
"type": "string",
"format": "uuid",
"pattern": "^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$"
},
"optionalUuid": {
"anyOf": [{ "$ref": "#/$defs/uuid" }, { "type": "null" }]
},
"optionalSha256": {
"type": ["string", "null"],
"pattern": "^[0-9a-f]{64}$"
},
"sha256List": {
"description": "SHA-256 hashes of JPEG attachments. Absent or null means an empty list.",
"type": ["array", "null"],
"items": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
"default": []
},
"accessoryList": {
"description": "Names of accessories, compared case-insensitively. Absent or null means an empty list.",
"type": ["array", "null"],
"items": { "type": "string", "minLength": 1 },
"default": []
},
"penalty": {
"description": "Contractual penalty for late return, in daily rates that rise with the delay. For late day d (1 = the first day after due_at) the rate is per_day of the tier with the greatest after_days below d; days before the first tier cost nothing. The sum over all late days is limited by the smaller of cap_amount and cap_percent of the total replacement value.",
"type": ["object", "null"],
"required": ["tiers"],
"properties": {
"tiers": {
"type": "array",
"items": {
"type": "object",
"required": ["after_days", "per_day"],
"properties": {
"after_days": { "description": "Late days that pass before this rate applies; 0 starts on the first late day.", "type": "integer", "minimum": 0 },
"per_day": { "description": "Amount per late calendar day in the inventory currency.", "type": "number", "minimum": 0 }
},
"additionalProperties": true
}
},
"cap_amount": { "description": "Upper limit of the total as an amount in the inventory currency.", "type": ["number", "null"], "minimum": 0 },
"cap_percent": { "description": "Upper limit of the total as a percentage of the total replacement value of the loan (sum of replacement_value times quantity over all items).", "type": ["number", "null"], "minimum": 0, "maximum": 100 }
},
"additionalProperties": true
}
},
"required": ["number", "borrower_id", "borrower", "issued_at", "items"],
"properties": {
"number": { "description": "Loan number as printed. The reference app numbers loans on the device as UE-{YYYY}-{DEV}-{SEQ:4}.", "type": "string" },
"borrower_id": { "description": "The recipient record of the borrower.", "$ref": "#/$defs/uuid" },
"borrower": { "description": "Copy of the borrower at handover, as on delivery notes.", "$ref": "delivery_note.json#/$defs/recipientCopy" },
"borrower_kind": { "description": "Kind of the borrower at handover. Absent or null: the reference app treats the borrower as an employee.", "type": ["string", "null"], "enum": ["customer", "site", "employee", null], "default": "employee" },
"borrower_department": { "description": "Copy of the employee's department at handover.", "type": ["string", "null"] },
"borrower_email": { "description": "Copy of the borrower's email address at handover, where the protocols can be sent.", "type": ["string", "null"] },
"purpose": { "description": "What the equipment is used for, for example a project.", "type": ["string", "null"] },
"note": { "description": "Remark printed on the handover protocol.", "type": ["string", "null"] },
"issued_at": { "description": "Time of the handover.", "type": "string", "format": "date-time" },
"issued_by": { "description": "Display name of the person handing out.", "type": ["string", "null"] },
"due_at": { "description": "Last day to return on: the start of that day in the time zone of the device that created the loan. Required for customers and sites, optional for employees.", "type": ["string", "null"], "format": "date-time" },
"items": {
"type": "array",
"items": {
"type": "object",
"required": ["position", "article_id", "quantity", "from_location_id", "movement_id"],
"properties": {
"position": { "description": "Line number, starting at 1. Returns refer to items by position.", "type": "integer" },
"article_id": { "$ref": "#/$defs/uuid" },
"sku": { "description": "Copy of the item number at handover. Absent: empty.", "type": "string" },
"name": { "description": "Copy of the article name at handover. Absent: empty.", "type": "string" },
"unit": { "description": "Copy of the unit at handover. Absent: \"Stk\".", "type": "string", "default": "Stk" },
"quantity": { "description": "Quantity handed out.", "type": "number" },
"serials": { "description": "Serial numbers handed out. Absent or null means an empty list.", "type": ["array", "null"], "items": { "type": "string" }, "default": [] },
"from_location_id": { "description": "Where the item left stock.", "$ref": "#/$defs/uuid" },
"location_code": { "description": "Copy of the location code at handover. Absent: empty.", "type": "string" },
"replacement_value": { "description": "Replacement value per unit in the inventory currency, the basis for compensation. Absent or null: the usual replacement value of a comparable item in corresponding condition applies, and documents say so.", "type": ["number", "null"], "minimum": 0 },
"condition": { "description": "Condition at handover in words, for example \"new\" or \"scratches on the lid\".", "type": ["string", "null"] },
"photos": { "description": "Condition photos at handover.", "$ref": "#/$defs/sha256List" },
"accessories": { "description": "Accessories handed out with the item. The loan stays open until each is returned or reported lost.", "$ref": "#/$defs/accessoryList" },
"movement_id": { "description": "The issue movement that booked this item out.", "$ref": "#/$defs/uuid" }
},
"additionalProperties": true
}
},
"signatures": {
"description": "Signatures at handover. The borrower's (role recipient) is required to create a loan, the issuer's is optional. Absent or null means none.",
"type": ["array", "null"],
"items": { "$ref": "delivery_note.json#/$defs/signature" },
"default": []
},
"returns": {
"description": "Returns in the order they were recorded. A return is only written once both the borrower (recipient) and the person taking the items back (issuer) have signed. Absent or null means none.",
"type": ["array", "null"],
"default": [],
"items": {
"type": "object",
"required": ["id", "returned_at", "items", "signatures"],
"properties": {
"id": { "description": "UUID of the return, unique within the loan.", "$ref": "#/$defs/uuid" },
"returned_at": { "type": "string", "format": "date-time" },
"received_by": { "description": "Display name of the person who took the items back.", "type": ["string", "null"] },
"items": {
"type": "array",
"items": {
"type": "object",
"required": ["position", "quantity", "condition"],
"properties": {
"position": { "description": "position of the loan item this line returns.", "type": "integer" },
"quantity": { "description": "Units returned or reported lost with this line; 0 when the line only hands in or writes off accessories.", "type": "number" },
"serials": { "description": "Serial numbers returned or reported lost. Absent or null means an empty list.", "type": ["array", "null"], "items": { "type": "string" }, "default": [] },
"condition": { "description": "ok and damaged come back into stock with a receipt movement; lost closes the units without a movement.", "type": "string", "enum": ["ok", "damaged", "lost"] },
"note": { "description": "Remark, for example what is damaged.", "type": ["string", "null"] },
"photos": { "description": "Condition photos at return.", "$ref": "#/$defs/sha256List" },
"accessories": { "description": "Accessories of the item returned with this line.", "$ref": "#/$defs/accessoryList" },
"lost_accessories": { "description": "Accessories of the item reported lost with this line.", "$ref": "#/$defs/accessoryList" },
"to_location_id": { "description": "Where the units went back into stock; null for lost units and accessory-only lines.", "$ref": "#/$defs/optionalUuid" },
"movement_id": { "description": "The receipt movement that booked the units back; null when nothing was booked.", "$ref": "#/$defs/optionalUuid" }
},
"additionalProperties": true
}
},
"signatures": { "description": "Signatures of the borrower (recipient) and the person taking the items back (issuer).", "type": "array", "items": { "$ref": "delivery_note.json#/$defs/signature" } },
"pdf": { "description": "SHA-256 of the return protocol PDF.", "$ref": "#/$defs/optionalSha256" }
},
"additionalProperties": true
}
},
"terms": { "description": "The conditions exactly as the borrower accepted them, including the penalty clause when one applies. Frozen at handover.", "type": ["string", "null"] },
"penalty": { "description": "Penalty scale agreed at handover; null when none was agreed.", "$ref": "#/$defs/penalty" },
"pdf": { "description": "SHA-256 of the handover protocol PDF.", "$ref": "#/$defs/optionalSha256" }
},
"allOf": [
{
"if": { "properties": { "borrower_kind": { "enum": ["customer", "site"] } }, "required": ["borrower_kind"] },
"then": { "required": ["due_at"], "properties": { "due_at": { "type": "string" } } }
}
],
"additionalProperties": true,
"examples": [
{
"number": "UE-2026-3F-0001",
"borrower_id": "5E2A8C14-9D3B-4A7F-B6C0-1E4D7F2A9B35",
"borrower": { "kind": "site", "name": "Northring construction site", "number": "K-10042", "street": "Northring 12", "postal_code": "10115", "city": "Berlin", "country": "Germany", "contact": "Max Mustermann" },
"borrower_kind": "site",
"purpose": "Installation level 3",
"issued_at": "2026-10-12T11:00:00Z",
"issued_by": "Erika Mustermann",
"due_at": "2026-10-18T22:00:00Z",
"items": [
{
"position": 1,
"article_id": "4A7C2E91-0B3D-4F6A-8E15-7C9B2D4F6A80",
"sku": "200310",
"name": "Cordless drill 18 V",
"unit": "Stk",
"quantity": 1,
"serials": ["DR-1001"],
"from_location_id": "2B0E6C1A-7D44-4F1B-9C3E-5A8D2F6B1C07",
"location_code": "N-A01",
"replacement_value": 189.9,
"condition": "new",
"photos": ["c1479c9c070dfaaf003b828b11d691dc9a4d455bf6f693a44de9e1080400b162"],
"accessories": ["Charger", "Battery 4 Ah", "Case"],
"movement_id": "A3B5C7D9-2E4F-4061-8A3C-5E7F9B1D3F52"
}
],
"signatures": [
{ "role": "recipient", "name": "Max Mustermann", "signed_at": "2026-10-12T11:02:00Z", "image": "acc6bcb1913432eed9c60781607713d63c802ff23893d35b88b1903ed04c2f50" },
{ "role": "issuer", "name": "Erika Mustermann", "signed_at": "2026-10-12T11:02:30Z", "image": "557643b476f24dda4ec227e3f41c0e8070d765f37c74c3bfa0d9dcfa6720eeee" }
],
"returns": [
{
"id": "F1A3C5E7-9B2D-4F46-8A0C-2E4B6D8F0A17",
"returned_at": "2026-10-20T07:30:00Z",
"received_by": "Erika Mustermann",
"items": [
{
"position": 1,
"quantity": 1,
"serials": ["DR-1001"],
"condition": "damaged",
"note": "Chuck wobbles",
"photos": ["84358bf14527752ea15b31417c718a0e6a7a92ad904b2e1f856574695b9bb611"],
"accessories": ["Charger", "Case"],
"lost_accessories": [],
"to_location_id": "2B0E6C1A-7D44-4F1B-9C3E-5A8D2F6B1C07",
"movement_id": "C7E9A1B3-5D7F-4291-B3D5-7F9A1C3E5B64"
}
],
"signatures": [
{ "role": "recipient", "name": "Max Mustermann", "signed_at": "2026-10-20T07:31:00Z", "image": "f4ea7a3453ab1b8fdeeaf7ee8d9fd4104e43956b95855a61c1ef9f3e180d6f6d" },
{ "role": "issuer", "name": "Erika Mustermann", "signed_at": "2026-10-20T07:31:30Z", "image": "be7d9df48d637d1f1e1684b07569c572999cc50eca2862a73f7dc25798f2b082" }
],
"pdf": "e65034da1a35513b1e77bb281f4bcb7fbc39abd8712d54ddfea59aaa974a2db8"
}
],
"terms": "The borrower returns the equipment by the due date ...",
"penalty": { "tiers": [{ "after_days": 2, "per_day": 5 }, { "after_days": 14, "per_day": 10 }], "cap_amount": null, "cap_percent": 30 },
"pdf": "28b405d5ad469ff43888136a56c515bc0762856c8052bd09830433634d8997c0"
}
]
}