embeddable widget
Drop a doora button on your checkout.
One <script> tag, one doora.mount() call, done. Your users pick a doora handle, consent to share, and your backend gets back a token to exchange for the address - same handoff flow as our REST integration, just embedded.
3 minutes
Quickstart
Add the script + a mount target on your checkout page. The widget renders a branded button; clicking it opens a doora consent popup; when the user picks a handle and shares, we postMessage a one-time token back to your page.
<script src="https://www.doora.to/widget.js" defer></script>
<div id="doora-checkout"></div>
<script>
window.addEventListener('doora:ready', () => {
doora.mount('#doora-checkout', {
merchantId: 'swiggy', // your merchant slug
returnUrl: 'https://swiggy.com/checkout', // your page's URL
state: 'csrf-token-from-server', // optional, round-tripped
onComplete: ({ token, state }) => {
// POST { token } to your backend; backend swaps it for the
// address via /v1/handoff/exchange with your doora API key.
fetch('/api/our-backend/save-doora-address', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ token, state }),
});
},
onCancel: (reason) => console.log('user cancelled:', reason),
onError: (err) => console.error(err),
});
});
</script>Your backend then calls POST /v1/handoff/exchange with the token. That endpoint authenticates with your dk_live_… key and returns the address fields the user consented to share. See the handoff API reference.
declarative
Mount API
doora.mount(target, options) renders a button into the target element. Idempotent - calling it again on the same node replaces the previous button (useful when the merchant's page is a SPA).
| Option | Type | Required | Notes |
|---|---|---|---|
| merchantId | string | required | Your merchant slug, e.g. 'swiggy'. Lowercase, no spaces. |
| returnUrl | string | required | Origin must match an entry in your key's allowed_return_origins (set by the doora team). |
| state | string | optional | Round-tripped through the flow back to onComplete. Use it for CSRF or correlation IDs. |
| label | string | optional | Override the button text. Default: 'Use my doora address'. |
| onComplete | (payload: { token, state }) => void | optional | Fires once the user has consented. Hand the token to your backend. |
| onCancel | (reason: 'user' | 'closed') => void | optional | Fires if the user clicks 'cancel' or closes the popup. |
| onError | (err: Error) => void | optional | Fires on popup-blocker or network errors. |
Return value: { destroy(): void } - call destroy() to remove the widget from the DOM (e.g. on SPA route change).
server-side
Backend exchange
The widget hands your frontend a token. To get the actual address, your backend posts the token to /v1/handoff/exchange with your doora API key in the Authorization header. Two-step design keeps the key off the browser.
// In your backend (Node example):
const resp = await fetch('https://www.doora.to/api/v1/handoff/exchange', {
method: 'POST',
headers: {
'authorization': `Bearer ${process.env.DOORA_API_KEY}`,
'content-type': 'application/json',
},
body: JSON.stringify({ token }),
});
const data = await resp.json();
// data.handle - "username@label"
// data.digipin - 10-char DIGIPIN (XXX-XXX-XXXX)
// data.pin_code - India Post 6-digit PIN (legacy courier routing key)
// data.coordinates - { lat, lng, accuracy_m }
// data.address - { building, floor, unit, notes } - nulls where owner-hidden
// data.verification - { method, verified_to_m }
// data.consent - { consented_at, return_url, state }
// data.mode - "test" | "live", matches your key environmentFull field reference + error codes in the handoff API docs. Tokens are single-use and expire 5 minutes after issue.
trust model
Trust model - widget
The widget never sees your API key - the browser only ever handles a 5-minute single-use consent token, and the actual address swap happens server-side. The popup posts results only to your returnUrl's origin (validated against your key's allowlist), and the widget verifies every event comes from doora's origin before reading it. Worst-case caveat: popup blockers - always call mount() / checkout() inside a real click handler. Full trust model →
sandbox
Testing locally
When you're handed your first API key, you get a dk_test_… alongside the dk_live_… - the test key hits the same data plane but never bills against your metered quota. For widget development:
- Point
merchantIdat the same slug you use in production. The merchant slug is shared across test + live keys. - Add
http://localhost:3000(or whatever your dev port is) to your key'sallowed_return_originsbefore testing. Ping support to update it. - Use the live demo page to verify the popup, postMessage, and cancel flows end-to-end before integrating.
/v1/handoff/exchangewith a test key returns the same address-payload shape as a live key - the consent token is the gate, not the key environment, so test integrations exchange against real handles. Themodefield on the response tells you which key environment authenticated the call.
onboarding
Getting an API key
Reach out via /contact with your merchant name + the origin(s) where the widget will live. We'll send back a dk_test_… + dk_live_… pair scoped to your slug - integrate against test first, flip to live when you're ready.