API and Webhooks: Integrating TenancyDesk with Your Systems

Updated July 14, 2026

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

Generate API key:

  1. Settings โ†’ Integrations โ†’ API Keys
  2. Click Generate New API Key
  3. Name: "Production Integration" (descriptive)
  4. Scopes: Select permissions (read:deals, write:parties, etc.)
  5. 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:

  1. Obtain access token via OAuth flow
  2. Include in Authorization header
  3. 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:

PlanRequests/MinuteRequests/HourBurst Limit
Enterprise30015,000600

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 created
  • deal.updated - Deal details changed
  • deal.status_changed - Deal status transition (e.g. moved to Active)
  • deal.completed - Deal marked complete
  • deal.cancelled - Deal cancelled

Payment events (4):

  • payment.created - Payment schedule entry created
  • payment.received - Payment marked as paid
  • payment.overdue - Payment missed its due date
  • payment.bounced - Cheque bounced

Document events (4):

  • document.uploaded - New document added
  • document.verified - Document verified
  • document.rejected - Verification failed
  • document.expiring - Document expiring soon

Party events (3):

  • party.created - Party (tenant or owner) added
  • party.updated - Party details changed
  • party.kyc_completed - Party KYC completed

Task events (3):

  • task.created - Task created
  • task.completed - Task completed
  • task.overdue - Task past its due date

Compliance events (2):

  • compliance.issue_created - Compliance issue detected
  • compliance.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.updated event 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:

  1. Subscribe to deal.created webhook
  2. 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:

  1. Subscribe to payment.received
  2. Call QuickBooks API to create invoice
  3. Update deal with QB invoice ID

Pattern 3: Custom Dashboard

Use case: Real-time analytics dashboard

Implementation:

  1. Poll /deals endpoint every 5 minutes
  2. Aggregate data (deals by stage, revenue)
  3. Display in custom dashboard

Error Handling

HTTP Status Codes:

  • 200 OK - Success
  • 201 Created - Resource created
  • 400 Bad Request - Invalid parameters
  • 401 Unauthorized - Invalid/missing API key
  • 403 Forbidden - Insufficient permissions
  • 404 Not Found - Resource doesn't exist
  • 422 Unprocessable Entity - Validation failed
  • 429 Too Many Requests - Rate limit exceeded
  • 500 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:

  1. Error handling: Catch all HTTP errors, implement retry logic
  2. Idempotency: Use Idempotency-Key header for POST requests
  3. Rate limiting: Respect rate limits, implement backoff
  4. Webhook security: Always verify HMAC signatures
  5. Logging: Log all API calls for debugging
  6. Monitoring: Track API latency and error rates
  7. 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.

By subscribing, you agree to receive emails from TenancyDesk. Unsubscribe anytime.

Still need help?

Our support team is ready to assist you with any questions.

Contact Us