| name | leihs |
| description | Interact with leihs equipment lending system via REST API. Use when managing equipment, items, models, categories, users, or inventory pools in a leihs instance. Triggers: leihs, inventory, equipment, lending, items, models, pools, Ausleihe. |
Leihs API
Manage equipment inventory and lending via the leihs REST API. No browser needed.
Prerequisites
- A running leihs instance (e.g.,
https://leihs.example.org)
- An API token (see Authentication)
curl installed
Configuration
export LEIHS_URL="https://leihs.example.org"
export LEIHS_TOKEN="<api-token>"
If not set, ask the user before making any requests.
Architecture (Critical)
Leihs is a multi-service application. Each service has different auth:
| Service | Path | Token Auth | What It Manages |
|---|
| Admin | /admin/ | YES | Users, pools, groups, buildings, rooms, categories, suppliers |
| Inventory | /inventory/ | YES | Models, items, images, attachments, export (newer instances only) |
| Legacy | /manage/ | NO (session only) | Models, items, orders, contracts (older instances) |
| Borrow | /borrow/ | NO (GraphQL + session) | Reservations, orders, availability |
| Procure | /procure/ | NO (GraphQL + session) | Procurement requests |
Check what's available on your instance:
curl -s -o /dev/null -w "%{http_code}" "${LEIHS_URL}/admin/status"
curl -s -o /dev/null -w "%{http_code}" "${LEIHS_URL}/inventory/status"
If /inventory/status returns 503, the instance runs an older version — only the admin API is available for token-based automation.
Authentication
Header format:
Authorization: Token <40-char-hex-token>
Scopes (set when token is created):
| Scope | Access |
|---|
scope_read | Read data (GET requests) |
scope_write | Create/update data (POST/PUT/DELETE) |
scope_admin_read | Read admin resources |
scope_admin_write | Manage admin resources |
Verify your token:
curl -s "${LEIHS_URL}/my/auth-info" \
-H "Authorization: Token ${LEIHS_TOKEN}" \
-H "Accept: application/json"
Returns your user info, scopes, and pool access rights.
Creating tokens: Tokens are managed at /admin/users/{user-id}/api-tokens/ or through the admin UI under user settings.
URL Conventions (Important!)
These will bite you if you don't know them:
- Hyphens, not underscores:
/admin/inventory-pools/ NOT /admin/inventory_pools/
- Trailing slash for lists:
/admin/users/ (with slash) returns 200. /admin/users (without) returns 404.
- No trailing slash for details:
/admin/inventory-pools/{id} (no slash) returns 200. With slash returns 544.
- Pagination is 1-based:
?page=1&per-page=50. Page 0 causes "OFFSET must not be negative".
- 544 = "No handler": Custom status code meaning the route doesn't exist.
Admin API
All endpoints need Accept: application/json header. No CSRF token needed for admin mutations (unlike inventory API).
The admin API does NOT expose models or items — those live in the inventory or legacy service.
Discover Pools (Start Here)
curl -s "${LEIHS_URL}/admin/inventory-pools/" \
-H "Authorization: Token ${LEIHS_TOKEN}" \
-H "Accept: application/json"
Response Format
List responses are wrapped in a key matching the resource name:
{"inventory-pools": [...]}
{"users": [...]}
{"buildings": [...]}
{"user-api-tokens": [...]}
Extract with jq: | jq '.["inventory-pools"]'
Note: User/pool responses may include large base64-encoded avatar images (img32_url). Pipe through jq to extract specific fields if bandwidth matters.
Quick Reference
| Method | Endpoint | Description |
|---|
| GET | /admin/inventory-pools/ | List all pools |
| GET | /admin/inventory-pools/{id} | Pool detail (no trailing slash!) |
| GET | /admin/inventory-pools/{id}/users/ | Pool users with roles |
| GET | /admin/inventory-pools/{id}/groups/ | Pool groups |
| GET | /admin/inventory-pools/{id}/delegations/ | Pool delegations |
| GET | /admin/users/?page=1&per-page=50 | List users (paginated) |
| GET | /admin/users/{id} | User detail |
| GET | /admin/users/{id}/api-tokens/ | User's API tokens |
| GET | /admin/groups/ | List groups |
| POST | /admin/groups/ | Create group ({"name": "..."}) |
| DELETE | /admin/groups/{id} | Delete group (returns 204) |
| GET | /admin/buildings/ | List buildings |
| GET | /admin/rooms/ | List rooms |
| GET | /admin/categories/ | Category tree (nested) |
| GET | /admin/suppliers/ | List suppliers |
| GET | /admin/inventory-fields/ | Field definitions |
| GET | /admin/mail-templates/ | Email templates |
| GET | /my/auth-info | Current user + scopes |
Write Operations (POST/PUT/DELETE)
Admin mutations work with token auth and do not require CSRF tokens:
curl -s -X POST "${LEIHS_URL}/admin/groups/" \
-H "Authorization: Token ${LEIHS_TOKEN}" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"name": "New Group", "org_id": "example.org"}'
curl -s -X DELETE "${LEIHS_URL}/admin/groups/${GROUP_ID}" \
-H "Authorization: Token ${LEIHS_TOKEN}"
Conventions for writes:
- POST to collection endpoint (with trailing slash):
/admin/groups/
- DELETE/PUT to resource endpoint (no trailing slash):
/admin/groups/{id}
- Always include
Content-Type: application/json for POST/PUT
Example: List Pool Users with Roles
curl -s "${LEIHS_URL}/admin/inventory-pools/${POOL_ID}/users/" \
-H "Authorization: Token ${LEIHS_TOKEN}" \
-H "Accept: application/json"
Inventory API (Newer Instances)
Available when /inventory/status returns 200. Has OpenAPI docs:
curl -s "${LEIHS_URL}/inventory/openapi.json" | jq '.paths | keys'
All inventory endpoints are scoped to a pool: /inventory/{pool_id}/...
| Method | Endpoint | Description |
|---|
| GET | /inventory/{pool_id}/models/ | Search models |
| GET | /inventory/{pool_id}/models/{id} | Model detail |
| POST | /inventory/{pool_id}/models/ | Create model |
| GET | /inventory/{pool_id}/items/ | List items (rich filters) |
| GET | /inventory/{pool_id}/category-tree/ | Category hierarchy |
| GET | /inventory/{pool_id}/buildings/ | Buildings |
| GET | /inventory/{pool_id}/rooms/ | Rooms |
| GET | /inventory/{pool_id}/templates/ | Reservation templates |
| GET | /inventory/{pool_id}/suppliers/ | Suppliers |
| GET | /inventory/{pool_id}/entitlement-groups/ | Access groups |
| GET | /inventory/{pool_id}/export/csv | CSV export |
| GET | /inventory/{pool_id}/export/excel | Excel export |
CSRF for mutations: POST/PUT/DELETE may require x-csrf-token header:
CSRF=$(curl -s "${LEIHS_URL}/inventory/csrf-token/" | jq -r '.["csrf-token"]')
curl -X POST ... -H "x-csrf-token: ${CSRF}"
Error Handling
| Status | Meaning | Fix |
|---|
| 302 | Redirect to login | Token not accepted by this service (legacy/borrow) |
| 401 | Not authenticated | Check token value and expiry (default 90 days) |
| 403 | Not authorized | Token scopes insufficient |
| 404 | Not found | Check trailing slash convention |
| 503 | Service unavailable | Service not deployed on this instance |
| 544 | No handler | Route doesn't exist — check hyphens and path |
Limitations
- Models/items on older instances: Only accessible via legacy
/manage/ (session auth, no token support). Use browser automation as fallback.
- Borrow/orders: GraphQL at
/borrow/graphql — requires session + CSRF, not token auth.
- Procurement: GraphQL at
/procure/graphql — session only.
- Token expiry: Default 90 days. Check with
/admin/users/{id}/api-tokens/.
- Search on admin: The
search-term parameter on some admin endpoints may not filter results.
Self-Improvement
If you encounter an endpoint that behaves differently, a missing operation, or unclear guidance:
- Create a PR on the skill repo fixing the issue directly
- Or file an issue with: what failed, actual behavior, and suggested fix
Never silently work around a skill gap.