api · v1
The doora merchant API.
One handle in, structured address payload out. Built for couriers, Q-commerce, food, ride-share, and field-service backends that already handle deliveries and want to stop parsing addresses by hand.
60-second integration
Quickstart
Get an API key from your doora contact (we issue them by hand for the first 10 merchants - write to contact). Pick your language - we'll remember it across the rest of this page:
curl https://www.doora.to/api/v1/resolve?handle=praneeth@home \
-H "Authorization: Bearer dk_test_<your-key>"A 200 response carries the full payload - coordinates, DIGIPIN, the address fields the owner has chosen to share, verification metadata, the mode (test/live) that authenticated the call, and an audit id you can reference in support tickets:
{
"handle": "praneeth@home",
"digipin": "4P3-JK8-52C9",
"pin_code": "500032",
"place_type": "apartment",
"coordinates": {
"lat": 17.4408,
"lng": 78.4886,
"accuracy_m": 4
},
"address": {
"building": "Phoenix Towers",
"floor": "4",
"unit": null,
"notes": "gate 2, the side entrance"
},
"verification": {
"method": "walk_and_lock",
"verified_to_m": 4
},
"mode": "test",
"resolved_at": "2026-05-25T20:00:00Z",
"audit_id": "2c1d…"
}bearer tokens
Authentication
Every request carries an Authorization: Bearer dk_<env>_<key> header. Keys are prefixed (dk_test_ or dk_live_), 40 chars total, and never re-displayed after issuance - we keep a one-way hash only, so a lost key has to be revoked and re-issued.
- Keep keys in your secret store. Never ship them to a client.
- Use a different key per environment - never share one across staging and production.
- Revocation is soft and instant - once revoked, the key returns
401on the next call.
environments
Test vs live keys
doora ships two key types. Use them like Stripe's: develop and CI against test, switch to live for production.
dk_test_…mode: "test" and never count toward your quota.dk_live_…mode: "live" and count against the tier in your contract.Why the restriction on test keys: without it, anyone with a test key could direct-resolve a real handle for free. The handoff flow (below) is different - there the user has explicitly consented in the doora UI, so test keys exchange tokens against real handles normally.
what test keys can see
Demo handles
A curated set of handles owned by the demo account. These behave like real handles in every way - full payload, DIGIPIN, visibility flags, error codes - so you can exercise every code path in your integration without touching real user data.
demo@homedemo@officebuilding + floor visible; unit hidden (returns null). Verifies your null-handling logic.demo@gatedemo@privateprivacyMode="private". Returns 404 - verify your handler treats this the same as "doesn't exist".404 from a test key, even if a real handle by that name exists. Treat it as "no such handle".per-key
Rate limits
Each key has a per-minute budget. The default is 60 RPM; ask via /contact if you need it lifted. When exceeded, the response is 429 with Retry-After (seconds) and X-RateLimit-Remaining: 0. We deliberately don't advertise the concrete per-key budget over the wire - honour Retry-After and back off.
The shape of a well-behaved backoff loop:
# Read the Retry-After header on 429 and sleep before retry.
RESP=$(curl -s -D - "https://www.doora.to/api/v1/resolve?handle=praneeth@home" \
-H "Authorization: Bearer dk_test_<your-key>")
# parse Retry-After: 8 → sleep 8 → retryendpoint
GET /v1/resolve
Authorization: Bearer dk_… requiredhandle - the doora handle in the form username@label (e.g. praneeth@home). Required.handle, digipin, pin_code, place_type, coordinates, address, verification, mode, resolved_at, audit_id.pin_code is the 6-digit India Post code (e.g. 500032) - the legacy routing key every traditional courier (DTDC, Bluedart, India Post, Delhivery) uses. null when the owner skipped it or the reverse-geocode found no coverage. For modern routing prefer digipin.place_type is one of apartment, villa, independent_house, commercial, or other. Use it to phrase the right label per field (the same address.unit value reads as "flat 1204" for an apartment but "suite 1404" for a commercial building). Handles created before this field was introduced surface as other.address.building, floor, unit, notes each respect the owner's per-field visibility flag AND the place type's field relevance (a villa never returns a floor, an independent house never returns a unit). When hidden, the field is present in the response as null - the key never disappears. Merchants always see the same shape.privacyMode="private" return 404. Treat it the same as a missing handle.let users bring their own handle
Handoff flow
Some integrations don't want to ask a customer to paste a doora handle into an address field. The handoff flow lets you add a single button - Use your doora handle - that sends the customer to doora, lets them pick (or claim) a handle, and redirects back to your callback with a one-time token. Your backend then exchanges that token for the address payload.
The user-facing UX:
merchant page doora.to merchant page
[Use your doora handle]
▼ click
▶ redirect ────▶ /handoff?merchant=…
&return=…
&state=…
▼ sign in (if needed)
▼ pick handle
▼ consent screen
◀ redirect back ──────────▶ /callback?token=…
&state=…
▼ POST /v1/handoff/exchange
◀── address payloadOn your side, the only HTML you ship is the entry link:
<a href="https://doora.to/handoff?merchant=YOUR_MERCHANT_ID&return=https%3A%2F%2Fyour-site.com%2Fcallback&state=YOUR_RANDOM_CSRF">
Use your doora handle
</a>Generate state as a random per-request CSRF string and store it server-side keyed by the user's session. On the callback, exchange the token, then verify the consent.state in the response equals the one you stored. return must be a URL we trust - either any HTTPS origin (default for dev keys) or one in your registered allowlist.
endpoint
POST /v1/handoff/exchange
Authorization: Bearer dk_… requiredcontent-type: application/json required{ "token": "<one-time>" } - the value of the ?token= query param doora appended to your return URL.GET /v1/resolve (handle, digipin, pin_code, place_type, coordinates, address, verification, mode, resolved_at, audit_id) plus a consent block: { consented_at, return_url, state }. Verify consent.state matches what you sent.merchant_id doesn't match the one the token was issued to.The exchange call, in each language:
curl -X POST https://www.doora.to/api/v1/handoff/exchange \
-H "Authorization: Bearer dk_test_<your-key>" \
-H "content-type: application/json" \
-d '{"token":"<one-time-token-from-return-url>"}'responses
Error codes
Every error response is { "error": "..." }. Status code is the source of truth - message text may change.
^[a-z0-9-]{3,24}@[a-z0-9-]{2,16}$.resolve.Retry-After.audit_id from any 2xx that preceded the failure in support and we'll trace.what the payload means
Field reference
username@label. The canonical user-facing identity. Use it for receipts, dashboards, and order logs - it's the same string the customer types into your address field.XXX-XXX-XXXX (12 chars with the two separators), ≈ 4m precision. For systems already consuming DIGIPIN (govt, India Post), pass this through directly.null. notes is free-form but capped at 240 chars.walk_and_lock, manual_satellite, unverified. walk_and_lock is the highest-confidence path - owner stood at the door while we averaged GPS samples."test" or "live" - mirrors the environment of the key that authenticated the request. Use this to sanity-check your secret store ("did I just ship a test key to prod?") and to gate any downstream behaviour that should only fire in production.how this fits your compliance posture
Privacy + DPDP
- Each resolve is logged with merchant attribution. The owner can request their resolution log via /me/account.
- The owner controls per-field visibility - your integration should expect
nullvalues and degrade gracefully (e.g. fall back to coordinates + map preview ifbuildingis null). - Don't store the raw address on your side for longer than you need it for the order. Reach out via /contact if you need a data-retention contract.
- For consent-required mode (Phase 2): a future
consent_tokenin the response will indicate whether the owner has pre-granted access for this specific transaction. v1 treats all non-private resolves as consented at the platform level.