API reference
Every v1 endpoint, honestly labeled. Live endpoints work today with an API key; Planned endpoints are on the published roadmap and return 404 for now.
Using an AI assistant? 🤖
Our MCP server exposes all 50 live v1 endpoints as tools for Claude Desktop, Muse, and other MCP clients, with full CRUD and confirm:true protection on destructive actions. Set up the Tenaro MCP server →
Authentication
Every request carries your key in the Authorization header. Keys are workspace-scoped. A key can only ever see its own workspace.
# Header format
Authorization: Bearer tnr_yourworkspace_<48-hex-chars>
Key format
tnr_ + workspace slug + _ + 48 hex chars. Only the SHA-256 hash is stored. We can't recover a lost key, only revoke and reissue.
Scopes
Keys are hierarchical: read < write < delete < admin. GET needs read; POST/PUT/PATCH need write; DELETE needs delete; user invite/disable needs admin. Request your level in the dashboard (Settings โ API access). The owner approves it. admin is never self-serviceable.
Rate limits
600 requests/minute per key. Exceed it and you'll get 429 with a Retry-After header. Failed-auth attempts are limited per IP.
Pagination
Every list endpoint is cursor-paginated. Never assume a list is complete after one call.
# Request GET /api/v1/properties?limit=2 # Response { "data": [ โฆ ], "pagination": { "has_more": true, "next_cursor": "42", "limit": 2 } } # Next page: GET /api/v1/properties?limit=2&starting_after=42
Authentication
API index
Discovery: version, workspace, your key's scopes, and the full live endpoint catalog.
Properties & Units
List properties
Paginated list of properties in the authenticated workspace. Filter by type, city, state, or country.
Query parameters
| Name | Type | Description |
|---|---|---|
limit | integer | 1โ100, default 20 |
starting_after | string | Cursor from pagination.next_cursor |
property_type | string | e.g. single_family, multi_family, condo |
city | string | Exact match |
state | string | Exact match |
country | string | Exact match |
{
"data": [
{
"id": "7",
"name": "Maple Apartments",
"property_type": "multi_family",
"address": {
"line1": "123 Maple St", "city": "Austin",
"state": "TX", "zip": "78701",
"formatted": "123 Maple St, Austin, TX 78701"
},
"total_units": 12,
"unit_count": 12
}
],
"pagination": { "has_more": false, "next_cursor": null, "limit": 20 }
}
Get property
Full detail for one property, including formatted address and unit count.
{
"data": [
{
"id": "7",
"name": "Maple Apartments",
"property_type": "multi_family",
"address": { โฆ },
"year_built": 1998,
"total_units": 12,
"purchase_price": 1450000.0,
"created_at": "2026-03-14 10:22:31",
"updated_at": "2026-09-30 08:11:02"
}
]
}
Create property
Scope: write. Requires: name. JSON body.
{
"data": [
{
"id": "7",
"name": "Maple Apartments",
"property_type": "multi_family",
"address": { โฆ },
"year_built": 1998,
"total_units": 12,
"purchase_price": 1450000.0,
"created_at": "2026-03-14 10:22:31",
"updated_at": "2026-09-30 08:11:02"
}
]
}
Replace property
Scope: write. Full field set required.
{
"data": [
{
"id": "7",
"name": "Maple Apartments",
"property_type": "multi_family",
"address": { โฆ },
"year_built": 1998,
"total_units": 12,
"purchase_price": 1450000.0,
"created_at": "2026-03-14 10:22:31",
"updated_at": "2026-09-30 08:11:02"
}
]
}
Update property
Scope: write. Partial update.
{
"data": [
{
"id": "7",
"name": "Maple Apartments",
"property_type": "multi_family",
"address": { โฆ },
"year_built": 1998,
"total_units": 12,
"purchase_price": 1450000.0,
"created_at": "2026-03-14 10:22:31",
"updated_at": "2026-09-30 08:11:02"
}
]
}
Delete property
Scope: delete. Hard delete (units cascade).
List units
Filter by property_id or status.
Query parameters
| Name | Type | Description |
|---|---|---|
property_id | integer | Exact match |
status | string | Exact match |
limit | integer | 1โ100, default 20 |
starting_after | string | Cursor |
Get unit
One unit.
Create unit
Scope: write. Requires: property_id, unit_label.
Update unit
Scope: write. Partial update.
Delete unit
Scope: delete.
Tenants
List tenants
Search by name/email; filter by is_active.
Query parameters
| Name | Type | Description |
|---|---|---|
search | string | Matches name/email |
is_active | boolean | Filter by active flag |
limit | integer | 1โ100, default 20 |
starting_after | string | Cursor |
Get tenant
One tenant (safe fields).
Create tenant
Scope: write. Requires: email. A temporary password is generated and returned ONCE if omitted.
Update tenant
Scope: write. Partial update.
Deactivate tenant
Scope: delete. Soft-delete: leases and history are preserved.
Vendors
List vendors
Vendor directory with profiles. Filter by search text or trade.
Query parameters
| Name | Type | Description |
|---|---|---|
search | string | Matches name/email/company |
trade | string | Matches trade_expertise |
limit | integer | 1โ100, default 20 |
starting_after | string | Cursor |
Get vendor
One vendor with profile.
Create vendor
Scope: write. Requires: email. Optional profile fields (company_name, trade_expertise, hourly_rate, โฆ).
Update vendor
Scope: write. Updates user and profile fields.
Deactivate vendor
Scope: delete. Soft-delete: job history preserved.
List vendor messages
Filter by vendor_user_id or channel.
Get vendor message
One message.
Log vendor message
Scope: write. Requires: vendor_user_id, channel.
Leases
List leases
Filter by unit_id, tenant_user_id, or status.
Query parameters
| Name | Type | Description |
|---|---|---|
unit_id | integer | Exact match |
tenant_user_id | integer | Exact match |
status | string | Exact match |
limit | integer | 1โ100, default 20 |
starting_after | string | Cursor |
Get lease
One lease.
Create lease
Scope: write. Requires: unit_id, tenant_user_id, start_date, end_date (YYYY-MM-DD), rent_amount.
Update lease
Scope: write. Partial update.
Delete lease
Scope: delete. Payments cascade.
Payments & Accounting
List payments
Filter by lease_id or status.
Query parameters
| Name | Type | Description |
|---|---|---|
lease_id | integer | Exact match |
status | string | Exact match |
limit | integer | 1โ100, default 20 |
starting_after | string | Cursor |
Get payment
One payment record.
Record payment
Scope: write. Requires: lease_id, amount (>0), due_date (YYYY-MM-DD). Payments cannot be deleted via the API.
Update payment
Scope: write. Update status / paid_at / payment_method_id.
List documents
Property document metadata (read-only). Filter by property_id or category.
Get document
One document's metadata.
Maintenance
List maintenance requests
Filter by property_id, status, priority, assigned_vendor_user_id.
Query parameters
| Name | Type | Description |
|---|---|---|
property_id | integer | Exact match |
status | string | Exact match |
priority | string | Exact match |
assigned_vendor_user_id | integer | Exact match |
limit | integer | 1โ100, default 20 |
starting_after | string | Cursor |
Get maintenance request
One request.
Create request
Scope: write. Requires: property_id, unit_id, tenant_user_id, title.
Update request
Scope: write. Status changes write a status-history entry (optional status_note).
Delete request
Scope: delete.
Users (admin)
List users
Safe fields only. Filter by role or search.
Get user
One user (safe fields).
Invite user
Scope: admin. Role must be PROPERTY_MANAGER, TENANT, or VENDOR: OWNER/ADMIN cannot be created via API.
Disable user
Scope: admin. OWNER/ADMIN accounts are protected.
Enable user
Scope: admin.
Campaigns
List campaigns
Filter by status or mode.
Get campaign
One campaign.
Create campaign
Scope: write. Creates a DRAFT. Requires: subject, body. Sending stays dashboard-only.
Update campaign
Scope: write. DRAFT campaigns only.
Delete campaign
Scope: delete. DRAFT campaigns only.
Roadmap
Exchange OAuth code
For third-party apps.
List invoices
Rent invoices and late fees.
Rent roll report
Delinquency report
Ask Tenaro AI
Natural-language query over your workspace.
Errors
One envelope for every failure. The request_id lets our team trace any call; docs links here.
{
"error": {
"code": "unauthorized",
"message": "Invalid API key.",
"request_id": "req_9f2ac41be077d311",
"docs": "https://tenaro.io/developers-reference.php#errors"
}
}
| HTTP | Code | Meaning |
|---|---|---|
400 | bad_request | Malformed parameters. |
401 | unauthorized | Missing, malformed, revoked, or expired key, or a key for a different workspace. |
403 | forbidden | Key lacks the required scope. |
404 | not_found | Unknown endpoint or resource id. Planned endpoints also return this until they ship. |
405 | method_not_allowed | Method not supported for this endpoint (e.g. DELETE on payments). |
422 | invalid_request | Validation failed: missing/invalid fields. The message names the field. |
429 | rate_limited | Slow down; honor Retry-After. |
500 | internal_error | Something broke on our side. Retry with backoff and contact us with the request_id. |
Changelog & versioning
v1 preview (2026-10-01)
Initial release. Live: GET /api/v1/properties, GET /api/v1/properties/{id}. Bearer auth, cursor pagination, rate limits.
Full workspace control (2026-10-02)
50 live endpoints: full CRUD for properties, units, tenants, vendors, leases, maintenance, payments, campaigns, vendor messages; read for users, documents; admin user invite/disable. Hierarchical scopes (read/write/delete/admin) with self-service requests and owner approval. Every mutation is audit-logged. MCP server mirrors all 50 endpoints.
Deprecation policy
Breaking changes get a new version (/v2/), never a silent change to /v1/. Deprecated versions receive 12 months' notice.
MCP server
Model Context Protocol server for Claude Desktop, Muse, and other MCP clients: 50 tools mirroring every live v1 endpoint, full CRUD. Destructive tools require an explicit confirm:true argument. Setup guide →
