Enterprise-grade REST API and real-time webhooks enable seamless integration between TenancyDesk and your existing business systems. Build custom workflows, automate data sync, and create powerful integrations.
API Overview
Architecture
RESTful API following industry standards:
- Resource-based URLs (
/deals,/parties,/documents) - HTTP methods: GET, POST, PUT, PATCH, DELETE
- JSON request/response bodies
- Standard HTTP status codes
- OpenAPI 3.0 specification available
Base URL: https://api.tenancydesk.com/v1/
API Documentation: https://docs.tenancydesk.com/api (interactive Swagger UI)
Authentication
API Keys (Recommended)
Generate API key:
- Settings โ Integrations โ API Keys
- Click Generate New API Key
- Name: "Production Integration" (descriptive)
- Scopes: Select permissions (read:deals, write:parties, etc.)
- Copy key immediately (shown only once)
Authentication header:
curl -H "Authorization: Bearer sk_live_abc123xyz..." \
https://api.tenancydesk.com/v1/deals
Security:
- Store securely (environment variables, not code)
- Rotate every 90 days
- Use different keys for dev/staging/prod
- Revoke immediately if compromised
JWT Bearer Tokens (OAuth 2.0 - Enterprise)
For user-context operations:
- Obtain access token via OAuth flow
- Include in Authorization header
- Token expires: 1 hour (refresh token: 7 days)
Use when: Building apps where users authenticate with their TenancyDesk accounts
Core API Endpoints
Deals API
List all deals:
GET /v1/deals?status=active&limit=50&offset=0
Response 200:
{
"data": [
{
"id": "deal_abc123",
"property_address": "Unit 1205, Churchill Tower, Business Bay",
"tenant_name": "Omar Khalid",
"annual_rent": 85000,
"status": "active",
"ejari_number": "12345678",
"created_at": "2025-01-15T10:30:00Z"
}
],
"pagination": {
"total": 127,
"limit": 50,
"offset": 0,
"has_more": true
}
}
Create deal:
POST /v1/deals
Content-Type: application/json
{
"property_address": "Unit 1205, Churchill Tower",
"tenant": {
"name": "Omar Khalid",
"emirates_id": "784-1988-1234567-1",
"mobile": "+971501234567"
},
"annual_rent": 85000,
"start_date": "2025-02-01",
"payment_schedule": "4_cheques"
}
Response 201:
{
"data": {
"id": "deal_abc123",
"status": "draft",
...
}
}
Get deal details:
GET /v1/deals/{deal_id}
Response 200: Full deal object with nested parties, documents, payments
Update deal:
PATCH /v1/deals/{deal_id}
{
"annual_rent": 90000,
"status": "active"
}
Parties API
Create tenant:
POST /v1/parties/tenants
{
"name": "Fatima Ali",
"emirates_id": "784-1990-2345678-2",
"mobile": "+971509876543",
"email": "fatima@email.com"
}
Search parties:
GET /v1/parties?search=Ahmed&type=tenant&limit=20
Documents API
Upload document:
POST /v1/deals/{deal_id}/documents
Content-Type: multipart/form-data
file: @emirates-id-front.pdf
category: tenant_emirates_id
Response 201:
{
"data": {
"id": "doc_xyz789",
"category": "tenant_emirates_id",
"filename": "emirates-id-front.pdf",
"extracted_data": {
"name": "Omar Mohammed Khalid",
"id_number": "784-1988-1234567-1",
"expiry_date": "2028-12-31"
},
"verification_status": "verified"
}
}
Payments API
List payment schedule:
GET /v1/deals/{deal_id}/payments
Response: Array of payment installments with due dates, amounts, status
Mark payment as received:
PATCH /v1/payments/{payment_id}
{
"status": "paid",
"paid_date": "2025-02-01",
"transaction_ref": "CHQ-12345"
}
Rate Limits
API access is Enterprise-only. Default rate limits:
| Plan | Requests/Minute | Requests/Hour | Burst Limit |
|---|---|---|---|
| Enterprise | 300 | 15,000 | 600 |
Rate limit headers (returned in every response):
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 847
X-RateLimit-Reset: 1640000000
429 Too Many Requests:
{
"error": {
"code": "rate_limit_exceeded",
"message": "Rate limit exceeded. Retry after 60 seconds.",
"retry_after": 60
}
}
Best practice: Implement exponential backoff when you receive 429
Webhooks
Overview
Real-time notifications when events occur in TenancyDesk. No polling required.
Delivery guarantee: At-least-once delivery with automatic retries
Signature verification: HMAC-SHA256 for security
Webhook Setup
1. Create webhook endpoint (your server):
# Flask example
from flask import Flask, request
import hmac
import hashlib
app = Flask(__name__)
@app.route('/webhook', methods=['POST'])
def webhook():
# Verify signature
signature = request.headers.get('X-TenancyDesk-Signature')
payload = request.data
# Compute expected signature
secret = 'your_webhook_secret'
expected = hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
if signature != expected:
return 'Invalid signature', 401
# Process event
event = request.json
if event['type'] == 'payment.overdue':
send_reminder(event['data'])
return 'OK', 200
2. Register webhook in TenancyDesk:
POST /v1/webhooks
{
"url": "https://your-domain.com/webhook",
"events": ["payment.overdue", "deal.status_changed"],
"description": "Production webhook for payment reminders"
}
Response 201:
{
"data": {
"id": "wh_abc123",
"url": "https://your-domain.com/webhook",
"secret": "whsec_xyz789...", # Use for signature verification
"events": ["payment.overdue", "deal.status_changed"],
"status": "active"
}
}
Webhook Events
TenancyDesk emits 21 event types across six categories.
Deal events (5):
deal.created- New deal createddeal.updated- Deal details changeddeal.status_changed- Deal status transition (e.g. moved to Active)deal.completed- Deal marked completedeal.cancelled- Deal cancelled
Payment events (4):
payment.created- Payment schedule entry createdpayment.received- Payment marked as paidpayment.overdue- Payment missed its due datepayment.bounced- Cheque bounced
Document events (4):
document.uploaded- New document addeddocument.verified- Document verifieddocument.rejected- Verification faileddocument.expiring- Document expiring soon
Party events (3):
party.created- Party (tenant or owner) addedparty.updated- Party details changedparty.kyc_completed- Party KYC completed
Task events (3):
task.created- Task createdtask.completed- Task completedtask.overdue- Task past its due date
Compliance events (2):
compliance.issue_created- Compliance issue detectedcompliance.issue_resolved- Compliance issue resolved
Ejari/Tawtheeq registration has no dedicated webhook events โ TenancyDesk does not integrate with the DLD. When your team records a registration number or updates a deal's registration status, that change is delivered through the
deal.updatedevent above.
Webhook Payload Structure
Standard envelope:
{
"id": "evt_abc123",
"type": "payment.overdue",
"created_at": "2025-12-26T10:30:00Z",
"data": {
"deal_id": "deal_xyz789",
"payment_id": "pay_123",
"tenant_name": "Omar Khalid",
"amount": 21250,
"due_date": "2025-01-01",
"cheque_number": "CHQ-001"
},
"company_id": "comp_abc"
}
Retry Logic
TenancyDesk follows Hook0 industry standard:
Fast retries (first 10 attempts):
- Attempt 1: Immediate
- Attempt 2: 5 seconds
- Attempt 3: 25 seconds
- Attempt 4: 125 seconds (2 min)
- Attempts 5-10: Exponential backoff up to 5 minutes
Slow retries (attempts 11-60):
- Every 1 hour
- Up to 60 total attempts (2.5 days)
Give up after: 60 failed attempts
Your endpoint should:
- Respond within 5 seconds
- Return 200-299 status code
- Process asynchronously (don't block response)
Security
Webhook signature verification (HMAC-SHA256):
Header: X-TenancyDesk-Signature
Compute expected signature:
import hmac
import hashlib
def verify_webhook(payload, signature, secret):
expected = hmac.new(
secret.encode('utf-8'),
payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
# Usage
if not verify_webhook(request.body, request.headers['X-TenancyDesk-Signature'], webhook_secret):
return 'Unauthorized', 401
Security best practices:
- Always verify signatures
- Use HTTPS only (webhooks rejected if HTTP)
- Implement idempotency (same event may be delivered twice)
- Log all webhook attempts for debugging
Common Integration Patterns
Pattern 1: Sync Deals to CRM
Use case: Every TenancyDesk deal auto-creates in Zoho CRM
Implementation:
- Subscribe to
deal.createdwebhook - When received, call Zoho CRM API:
# TenancyDesk webhook handler if event['type'] == 'deal.created': deal = event['data'] zoho.create_lead({ 'name': deal['tenant_name'], 'value': deal['annual_rent'], 'source': 'TenancyDesk' })
Pattern 2: Automate Accounting
Use case: Payment received โ create invoice in QuickBooks
Implementation:
- Subscribe to
payment.received - Call QuickBooks API to create invoice
- Update deal with QB invoice ID
Pattern 3: Custom Dashboard
Use case: Real-time analytics dashboard
Implementation:
- Poll
/dealsendpoint every 5 minutes - Aggregate data (deals by stage, revenue)
- Display in custom dashboard
Error Handling
HTTP Status Codes:
200 OK- Success201 Created- Resource created400 Bad Request- Invalid parameters401 Unauthorized- Invalid/missing API key403 Forbidden- Insufficient permissions404 Not Found- Resource doesn't exist422 Unprocessable Entity- Validation failed429 Too Many Requests- Rate limit exceeded500 Internal Server Error- Server issue (retry)
Error response format:
{
"error": {
"code": "validation_failed",
"message": "Annual rent must be greater than 0",
"field": "annual_rent",
"docs_url": "https://docs.tenancydesk.com/api/errors#validation"
}
}
Handling errors:
response = requests.post(url, headers=headers, json=data)
if response.status_code == 422:
error = response.json()['error']
print(f"Validation failed on {error['field']}: {error['message']}")
elif response.status_code == 429:
retry_after = int(response.headers['Retry-After'])
time.sleep(retry_after)
# Retry request
elif response.status_code >= 500:
# Exponential backoff retry
pass
Best Practices
Production-ready integrations:
- Error handling: Catch all HTTP errors, implement retry logic
- Idempotency: Use
Idempotency-Keyheader for POST requests - Rate limiting: Respect rate limits, implement backoff
- Webhook security: Always verify HMAC signatures
- Logging: Log all API calls for debugging
- Monitoring: Track API latency and error rates
- Versioning: Use API version header if pinning to specific version
Example production-grade request:
import requests
import time
def create_deal_with_retry(deal_data, max_retries=3):
headers = {
'Authorization': f'Bearer {API_KEY}',
'Content-Type': 'application/json',
'Idempotency-Key': f'deal_{uuid4()}',
'X-API-Version': '2025-12-01'
}
for attempt in range(max_retries):
try:
response = requests.post(
'https://api.tenancydesk.com/v1/deals',
headers=headers,
json=deal_data,
timeout=10
)
if response.status_code == 201:
return response.json()['data']
elif response.status_code == 429:
wait = int(response.headers.get('Retry-After', 60))
time.sleep(wait)
continue
elif response.status_code >= 500:
time.sleep(2 ** attempt) # Exponential backoff
continue
else:
response.raise_for_status()
except requests.exceptions.Timeout:
if attempt == max_retries - 1:
raise
time.sleep(2 ** attempt)
raise Exception("Max retries exceeded")
Build robust integrations that scale with your business. TenancyDesk API designed for production-grade integrations with established cloud infrastructure.
Was this article helpful?
Related Articles
Stay Updated
Get the latest guides, tips, and updates delivered to your inbox.