Developers
Build on your portfolio with the DoorKeeping API.
Read your units, rent roll, ledgers and work orders, make changes, and get a signed message when something happens. Free and included in the $2 per unit price.
What the API does
The API runs the same tools your AI app uses through DoorKeeping's connector, with the same rules. You can read the portfolio summary, properties, units, the rent roll, leases, tenant ledgers, delinquency, work orders and approvals, and with a read and write key you can make the same changes your AI app can. The full list of tools, with every field, is in the OpenAPI file, and GET /api/v1/tools lists the tools your key may call.
Get a key
- Sign in to DoorKeeping and open Settings.
- Make an API key: give it a name, pick read or read and write, and limit it to some properties if you like.
- Copy the key. It is shown only once. Keys for your real portfolio start with
dk_live_; keys for the Sample portfolio start withdk_test_and are safe for trying anything.
Only a person signed in as an owner or staff member can make, list or revoke keys. Revoking is immediate. Keep keys on your server, never in a web page.
Send the key with every call
Put the key in the Authorization header. A missing, unknown or revoked key gets 401 unauthorized.
curl https://doorkeeping.com/api/v1/rent-roll \
-H "Authorization: Bearer dk_live_YOUR_KEY"Run a tool by name with a JSON body:
curl -X POST https://doorkeeping.com/api/v1/tools/create_work_order \
-H "Authorization: Bearer dk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"property_id": "...", "description": "The kitchen sink in 2B leaks", "confirm": true}'Answers are {"data": ...}; errors are {"error": {"code": "...", "message": "..."}}. Amounts are integer cents, dates are YYYY-MM-DD and times are UTC. Lists return next_cursor; pass it back as cursor.
Scopes
- Read keys call every read tool.
- Read and write keys also make changes. A change shows you its draft first; send
"confirm": trueto save it. - A key limited to some properties must pass a
property_idfrom its list and never sees rows from other properties.
Money and legal steps wait for you
Payments, signatures, legal notices and applicant decisions never happen through the API on their own. The call returns 202 with an approval link, and nothing happens until an owner taps Accept on doorkeeping.com.
Rate limits
Each key may make 60 calls a minute and 10,000 a day (UTC). Every answer carries X-RateLimit-Limit and X-RateLimit-Remaining. Over the limit you get 429 rate_limited with Retry-After in seconds.
Webhooks
Add your server's https address in Settings, under Webhooks, and pick the events you want. DoorKeeping posts each event as JSON: {"id", "type", "created", "org_id", "data"}. The data holds record ids and statuses, never a tenant's tax id, bank or card numbers. Use the event id to skip an event you already handled.
payment.received: Rent or another payment was matched to a lease.payment.failed: A payment failed or was returned.work_order.created: A work order was opened.work_order.updated: A work order changed status, schedule or helper.lease.signed: Everyone signed a lease, renewal or addendum.application.submitted: An applicant sent a rental application.approval.created: Something is waiting for your approval.approval.decided: An approval was accepted, rejected or expired.
Answer with a 2xx status within 5 seconds. Redirects are not followed. A failed send is retried with growing waits for about two days; after 20 failed sends in a row the webhook turns off and the owners get an email. Use Send test event in Settings to try your server.
Check the signature
Every post carries DoorKeeping-Signature: t=1730300000,v1=...: an HMAC-SHA256, in hex, of the timestamp, a dot and the raw body, made with the webhook's signing secret (shown once, when you add it). Refuse anything that does not match or is more than 5 minutes old.
import { createHmac, timingSafeEqual } from 'node:crypto'
// rawBody: the request body exactly as received (a string, before JSON.parse).
export function isFromDoorKeeping(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')))
const t = Number(parts.t)
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false
const want = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest()
const got = Buffer.from(parts.v1 ?? '', 'hex')
return got.length === want.length && timingSafeEqual(got, want)
}
// Express: app.post('/doorkeeping', express.text({ type: '*/*' }), (req, res) => {
// if (!isFromDoorKeeping(req.body, req.get('DoorKeeping-Signature'), process.env.DK_WEBHOOK_SECRET)) return res.sendStatus(400)
// const event = JSON.parse(req.body) // { id, type, created, org_id, data }
// res.sendStatus(200) // answer within 5 seconds; use event.id to skip repeats
// })Free and included
The API and webhooks come with every portfolio at $2 per unit a month. No setup cost, no minimum.