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

OptionTypeRequiredNotes
merchantIdstringrequiredYour merchant slug, e.g. 'swiggy'. Lowercase, no spaces.
returnUrlstringrequiredOrigin must match an entry in your key's allowed_return_origins (set by the doora team).
statestringoptionalRound-tripped through the flow back to onComplete. Use it for CSRF or correlation IDs.
labelstringoptionalOverride the button text. Default: 'Use my doora address'.
onComplete(payload: { token, state }) => voidoptionalFires once the user has consented. Hand the token to your backend.
onCancel(reason: 'user' | 'closed') => voidoptionalFires if the user clicks 'cancel' or closes the popup.
onError(err: Error) => voidoptionalFires 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 environment

Full 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 merchantId at 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's allowed_return_origins before 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/exchange with 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. The mode field 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.

Built something using the widget? We'd love to see it. Drop us a note.