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.

live · v1 <100ms p95 bearer auth

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:

Request
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 401 on 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_…
Test keys. Direct-resolve a fixed set of demo handles only (listed below). Responses carry mode: "test" and never count toward your quota.
dk_live_…
Live keys. Resolve any handle. Responses carry 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@home
Apartment in Hyderabad. All address fields visible, walk- and-lock verified to ~4 m. The happy path.
demo@office
Office building. building + floor visible; unit hidden (returns null). Verifies your null-handling logic.
demo@gate
Manual-satellite verification, larger accuracy. Tests the "low-precision pin" branch of your UI.
demo@private
Owner set to privacyMode="private". Returns 404 - verify your handler treats this the same as "doesn't exist".
anything else
Returns 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:

Retry with backoff
# 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 → retry

endpoint

GET /v1/resolve

Headers
Authorization: Bearer dk_… required
Query
handle - the doora handle in the form username@label (e.g. praneeth@home). Required.
Returns 200
The JSON payload shown in Quickstart. handle, digipin, pin_code, place_type, coordinates, address, verification, mode, resolved_at, audit_id.
PIN code
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
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.
Field visibility
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.
Privacy mode
Handles set to 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 payload

On 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

Headers
Authorization: Bearer dk_… required
content-type: application/json required
Body
{ "token": "<one-time>" } - the value of the ?token= query param doora appended to your return URL.
Returns 200
The same shape as 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.
403
token belongs to a different merchant. Your API key's merchant_id doesn't match the one the token was issued to.
404
token not found. Treat as a fresh handoff required.
410
token already exchanged or expired. Single-use, 5-minute TTL - re-prompt the user.

The exchange call, in each language:

Exchange
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.

400
handle malformed. Pattern is ^[a-z0-9-]{3,24}@[a-z0-9-]{2,16}$.
401
bearer missing or invalid. Covers unknown and revoked keys - we don't distinguish.
403
scope insufficient. v1 keys only ship resolve.
404
handle not found. Don't retry.
429
rate limit exceeded. Honour Retry-After.
500
internal. Quote the audit_id from any 2xx that preceded the failure in support and we'll trace.

what the payload means

Field reference

handle
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.
digipin
India Post's open national geocode. Always present where the pin is inside India: 10 alphanumeric characters rendered as XXX-XXX-XXXX (12 chars with the two separators), ≈ 4m precision. For systems already consuming DIGIPIN (govt, India Post), pass this through directly.
coordinates.lat / lng
WGS-84 decimal degrees. The pin the owner verified.
coordinates.accuracy_m
Worst-case distance from the actual door at lock time. Pins captured via walk-and-lock are typically ≤5 m; manual-satellite pins can be higher.
address.building / floor / unit / notes
Human-readable last-mile context. The owner controls per-field visibility - hidden fields come back as null. notes is free-form but capped at 240 chars.
verification.method
One of walk_and_lock, manual_satellite, unverified. walk_and_lock is the highest-confidence path - owner stood at the door while we averaged GPS samples.
mode
"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.
audit_id
A server-side request identifier. Quote it in support tickets and we'll trace the exact call in under a second.

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 null values and degrade gracefully (e.g. fall back to coordinates + map preview if building is 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_token in 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.