Developer hub

Developer Documentation & Quickstart

Integrate Digi-ID authentication into any back-end service using standard message verification libraries.

Core Concepts

  • Challenge Nonce: A random, time-bound string tied to the login session to prevent replay attacks.
  • Domain Binding: Key derivation uses your exact URI domain, ensuring keys generated for one website cannot authenticate on another.
  • Stateless Verification: Verification uses standard ECDSA signature checking on the server without pinging the blockchain or remote nodes.

Quickstart · ~5 minutes

Integrate passwordless authorization in three steps.

Your server issues a challenge nonce, the wallet signs it, and your server verifies the signature locally. Choose your backend language to view code samples.

  1. Step 1

    Install a library

    Verification needs no DigiByte node and no network calls.

    npm install digiid express
    # PHP 7.4+ with the GMP extension
    git clone --recursive https://github.com/DigiByte-Core/digiid-php
    php -m | grep -i gmp
    npm install qrcode   # render the challenge as a QR code
  2. Step 2

    Issue a challenge

    Create a random nonce, remember it for ~90 seconds, and show the digiid:// URI as a QR code and a link.

    // server.js — issue a challenge (Express)
    import crypto from 'node:crypto';
    import express from 'express';
    import DigiID from 'digiid';
    
    const CALLBACK = 'www.example.com/digiid/callback'; // host + path, no scheme
    const pending = new Map(); // nonce -> { sessionId, expires, address }
    
    const app = express();
    
    app.post('/digiid/challenge', (req, res) => {
      const nonce = crypto.randomBytes(16).toString('hex');
      pending.set(nonce, { sessionId: req.sessionID, expires: Date.now() + 90_000 });
      const { uri } = new DigiID({ nonce, callback: CALLBACK });
      res.json({ uri, nonce }); // digiid://www.example.com/digiid/callback?x=…
    });
    <?php
    // login.php — issue a challenge (digiid-php)
    require_once __DIR__ . '/DigiID.php';
    session_start();
    
    $digiid = new DigiID();
    $nonce  = $digiid->generateNonce();          // 32 hex chars from a CSPRNG
    
    // Bind the nonce to this session with a short expiry (see "Nonces & replay").
    $db->prepare('INSERT INTO digiid_nonces (nonce, session_id, expires_at) VALUES (?, ?, ?)')
       ->execute([$nonce, session_id(), time() + 90]);
    
    $uri = $digiid->buildURI('https://www.example.com/digiid/callback.php', $nonce);
    // Render $uri as a link and as a QR code (e.g. with endroid/qr-code).
    // login.js — show the challenge in the browser
    import QRCode from 'qrcode';
    
    const { uri, nonce } = await fetch('/digiid/challenge', { method: 'POST' }).then((r) => r.json());
    
    // Tappable on mobile (opens the wallet), scannable on desktop.
    document.querySelector('#digiid-link').href = uri;
    await QRCode.toCanvas(document.querySelector('#digiid-qr'), uri, { width: 240 });
    
    // Wait for the wallet to call back, then continue the session.
    const poll = setInterval(async () => {
      const { state } = await fetch(`/digiid/status?nonce=${nonce}`).then((r) => r.json());
      if (state === 'verified') {
        clearInterval(poll);
        location.assign('/account');
      }
    }, 2000);
  3. Step 3

    Verify the callback

    The wallet POSTs address, uri and signature. Check the URI, the nonce and the signature, then consume the nonce.

    // server.js — verify the wallet callback (Express)
    app.post('/digiid/callback', express.json(), express.urlencoded({ extended: false }), (req, res) => {
      const { address, uri, signature } = req.body;
      if (!address || !uri || !signature) return res.status(400).json({ error: 'missing fields' });
    
      const digiid = new DigiID({ address, uri, signature, callback: CALLBACK });
      const challenge = pending.get(digiid.nonce);
    
      if (!digiid.uriValid() || !challenge || challenge.expires < Date.now()) {
        return res.status(410).json({ error: 'unknown or expired challenge' });
      }
      if (!digiid.signatureValid()) return res.status(401).json({ error: 'invalid signature' });
    
      pending.delete(digiid.nonce);            // single use: a replay now fails
      sessions.login(challenge.sessionId, address); // address = the user's ID for your site
      res.json({ message: 'Digi-ID verified' });
    });
    <?php
    // callback.php — verify the wallet callback (digiid-php)
    require_once __DIR__ . '/DigiID.php';
    $digiid = new DigiID();
    
    // Wallets send JSON; manual signing tools send form fields.
    $input = json_decode(file_get_contents('php://input'), true) ?? $_POST;
    $address   = (string) ($input['address'] ?? '');
    $signature = (string) ($input['signature'] ?? '');
    $uri       = (string) ($input['uri'] ?? '');
    
    $nonce = $digiid->extractNonce($uri);
    $row = $db->prepare('SELECT session_id FROM digiid_nonces WHERE nonce = ? AND expires_at > ? AND address IS NULL');
    $row->execute([$nonce, time()]);
    $challenge = $row->fetch();
    
    $expectedUri = $digiid->buildURI('https://www.example.com/digiid/callback.php', $nonce);
    
    if (!$challenge || $uri !== $expectedUri
        || !$digiid->isMessageSignatureValidSafe($address, $signature, $uri)) {
        http_response_code(401);
        exit(json_encode(['error' => 'invalid or expired Digi-ID challenge']));
    }
    
    // Consume the nonce and attach the verified address to the waiting session.
    $db->prepare('UPDATE digiid_nonces SET address = ? WHERE nonce = ?')->execute([$address, $nonce]);
    echo json_encode(['message' => 'Digi-ID verified']);
    // Verify in the browser or any JS runtime with digiid-core (packages/digiid-core in the digi-id.io repo)
    import { verifyCallback } from './digiid-core/src/index.ts';
    
    const result = verifyCallback(
      { address, uri, signature },                          // POST body from the wallet
      { callback: 'https://www.example.com/digiid/callback' }
    );
    
    if (result.valid) {
      // result.nonce -> look up and consume the pending challenge
      // result.address -> the user's Digi-ID for your site
    } else {
      console.warn('Rejected:', result.reason); // bad_request | uri_mismatch | unsecure | bad_signature
    }

SDKs & plugins

Official SDKs & Reference Libraries

Pure-cryptography libraries that handle key derivation, URI parsing, and message signature verification without external API dependencies.

  • digiid-core

    TypeScript · browser & Node

    Maintained

    URI helpers, key derivation and signature verification that run in browsers and Node. Powers this site's demo and playground; tested against the published vectors.

    Copy packages/digiid-core from the digi-id.io repository Source on GitHub
  • digiid-js

    JavaScript · Node.js

    Community

    Lightweight npm package for Express, Fastify, and Next.js backends. Includes native TypeScript definitions.

    npm install digiid Source on GitHub
  • digiid-php

    PHP

    Community

    Supports PHP 7.4+ with GMP extension. Drop-in integration for standard web servers.

    git clone https://github.com/DigiByte-Core/digiid-php Source on GitHub
  • Digi-ID for WordPress

    WordPress plugin

    Unmaintained

    Enables passwordless sign-in for WP-Admin and subscriber logins out of the box.

    Upload to wp-content/plugins and activate Source on GitHub
  • Digi-ID for phpBB

    phpBB extension

    Unmaintained

    Native forum authentication plugin that replaces standard login forms.

    Copy to ext/DigiByte/digiid and enable in the ACP Source on GitHub

Protocol reference

How Digi-ID works, precisely.

Digi-ID is an open protocol for authentication with public-key cryptography, derived from the BitID draft. It works much like keyless SSH, with a wallet as the user-friendly key manager. Only the user's public address is ever stored by a service.

Challenge URI

Before granting access, the service shows a QR code (and, on mobile, a link) containing:

digiid://www.example.com/callback?x=NONCE
PartMeaning
digiid://Scheme that opens a Digi-ID wallet when tapped or scanned.
www.example.com/callbackCallback URL without scheme. HTTPS is assumed and strongly recommended.
x=NONCEUnique, unpredictable value linked to the user's session. Never reuse it.
u=1 (optional)Tells the wallet to POST over plain HTTP. For local development only — never in production. Digi-ID is not a substitute for TLS.

Prefix the QR code with “Sign in with Digi-ID” so users know what they are scanning. The DigiByte “D” in the QR code is optional.

Callback

After the user confirms with their PIN or fingerprint, the wallet POSTs to the callback URL (JSON body; some tools send form fields):

POST /callback HTTP/1.1
Host: digiid.digibyteprojects.com
Content-Type: application/json

{
  "address": "DJDAkjie6nrW6RpFZSTpNUXsZ9JE2x6p1o",
  "uri": "digiid://digiid.digibyteprojects.com/callback?x=c6140375e5bae71e",
  "signature": "H3tlK3RciMR60PuE0jE6JuClqgh+E8MmMz+n+4+P7FAIVuUccs+npnIa6Gz8XAJeOilTWpLNbomXtVDe4gR0CNk="
}
FieldDescription
addressThe user's DigiByte address for your site (starts with D). Use it as the stable user ID.
uriThe exact challenge URI that was signed.
signatureBase64, 65-byte compact recoverable ECDSA signature of uri.

Respond with 200 on success. Wallets show a success or error message to the user based on the response.

Verification

  1. Parse uri; confirm the scheme is digiid and host + path equal your callback.
  2. Reject u=1 in production.
  3. Look up the nonce: it must exist, be unexpired (≈90 s) and unused.
  4. Verify the signature: hash "\x19DigiByte Signed Message:\n" + varint(len) + uri with double SHA-256, recover the public key, derive the P2PKH address (version byte 0x1E) and compare with address.
  5. Mark the nonce as used and attach address to the waiting session.

No DigiByte node or external service is required — the libraries implement all of this locally.

Key derivation

Wallets derive a separate key per site, so users can't be tracked across services. The derivation input is the callback URL with its scheme and path, without the query string (for example https://www.example.com/callback). Keep one stable callback URL per site so user addresses stay the same.

  1. Concatenate the little-endian 32-bit index (usually 0) with the callback URL.
  2. Compute SHA-256 of the result.
  3. Split the first 128 bits into four little-endian 32-bit numbers A, B, C, D.
  4. Set the highest bit of each (hardened).
  5. Derive BIP32 node m/13'/A'/B'/C'/D'.

Test vector

BIP39 seedmyth glimpse mystery abstract embark net faint hospital catch hint develop state
Digi-ID URIdigiid://digiid.digibyteprojects.com/callback?x=c6140375e5bae71e
Index0
SHA-256460ebbfd1df7410106d62d41f802f769bc00db825e9cf8c649cb069dec35e8fb
BIP32 pathm/2147483661/4256894534/2168583965/3241006598/3925279480
AddressDJDAkjie6nrW6RpFZSTpNUXsZ9JE2x6p1o

The callback example above is a real signature from this vector — paste it into the playground to check it.

Security checklist

  • Serve the callback over HTTPS; never accept u=1 in production.
  • Generate nonces from a CSPRNG (≥128 bits), bind them to a session, expire them after ~90 s and delete them after first use.
  • Compare the signed URI with the one you issued — host, path and nonce.
  • Rate-limit the callback and challenge endpoints.
  • Treat the address as an identifier, not an authorization: map it to roles in your own system.
  • Offer a recovery path (e.g. link a second wallet) — a lost seed means a lost key.

More in the security model and FAQ.

Playground

Check a signature, right here.

Paste a callback payload to see which checks pass. Everything runs in your browser; nothing is sent anywhere.

Only paste public values: an address, a URI and a signature. Never paste a seed phrase or private key into any website.

Brand & UI kit

Make it recognisable.

A consistent “Sign in with Digi-ID” button and QR presentation help users trust the flow.

Sign-in button

Sign in with Digi-ID button, light Sign in with Digi-ID button, dark

QR code guidance

  • Label it “Sign in with Digi-ID” above the code.
  • At least 200 px, dark modules on a light background, quiet zone intact.
  • Also render the URI as a tappable link for mobile users.
  • Show a countdown and regenerate after expiry.
Back to top