Skip to content
Comms.ID
Esc
↑↓navigate↵open⌘Jpreview
On this page

Verify a supplier's ABN on your server

Verify a supplier’s ABN on your server

What you build: before a supplier is paid, your server checks the ABN they gave you. A typo is caught locally and costs no call; a well-formed number is looked up on the Australian Business Register; the answer tells you whether the entity is registered, its name and status, and which source answered. An outage is reported as an outage, never as “not registered”.

Before you start: a development app, then a production app

  1. npx comms-id init makes a development key in .env.local and prints its public key. In the Companion App, register it as a development app, enable ABN for it, and put the app id in .env.local as COMMS_ID_CLIENT_ID. This key reaches only the TEST service (https://api-test.comms.id): the same API, errors and authentication as LIVE, with synthetic replies.
  2. When it works in TEST, npx comms-id key --worker <your worker> makes the production key straight into your server’s COMMS_ID_PRIVATE_JWK secret and prints the public key; register it as a production app with ABN enabled. Going live changes only the environment and the key: nothing in the code below.

A .env.local never holds a production key, and a browser never holds any key.

The code

pnpm add @comms-id/abn

verify-supplier.ts:

// Verify a supplier's ABN on your server: check the number locally first (no call for a typo),
// then ask the register. An outage is reported as such, never as "not registered".
import { type AbnClient, outcomeOf } from "@comms-id/abn";
import { formatAbn, validateAbn } from "@comms-id/abn/local";

export type SupplierCheck =
  | { readonly ok: true; readonly abn: string; readonly name: string; readonly status: string; readonly source: string }
  | { readonly ok: false; readonly reason: "invalid" | "not-registered" | "unavailable"; readonly message: string };

export const verifySupplier = async (input: string, abn: AbnClient): Promise<SupplierCheck> => {
  const local = validateAbn(input);
  if (!local.valid) {
    return { ok: false, reason: "invalid", message: local.message };
  }
  try {
    const reply = await abn.lookup({ abn: local.abn });
    const entity = reply.entity;
    if (entity === null) {
      return { ok: false, reason: "not-registered", message: `${formatAbn(local.abn)} is not on the register.` };
    }
    // Both sources (the live register and the dated snapshot) carry the name and the status.
    return {
      ok: true,
      abn: formatAbn(local.abn),
      name: entity.entityName,
      status: entity.entityStatusCode ?? "unknown",
      source: reply.source,
    };
  } catch (error) {
    if (outcomeOf(error) === "source-unavailable") {
      return { ok: false, reason: "unavailable", message: "The register did not answer; try again later." };
    }
    throw error;
  }
};

validateAbn runs the check-digit rule in your process; the lookup reaches the register through the client you give the function, so the same function serves your tests (the fixtures client), TEST and LIVE.

TEST first

import { createAbnClient } from "@comms-id/abn/server";
import { verifySupplier } from "./verify-supplier";

const abn = createAbnClient({
  clientId: process.env.COMMS_ID_CLIENT_ID!,
  privateJwk: JSON.parse(process.env.COMMS_ID_PRIVATE_JWK!),
  environment: "test",
});

console.log(await verifySupplier("51 824 753 556", abn)); // the published example: found, with a name and status
console.log(await verifySupplier("51 824 753 557", abn)); // invalid check digit: refused here, no call
console.log(await verifySupplier("10000000000", abn)); // TEST's documented miss: not registered
console.log(await verifySupplier("10000000032", abn)); // TEST's documented outage: unavailable

The four calls above exercise a match, an invalid check digit, a missing registration and a source outage in TEST. In your own unit tests, createAbnFixtureClient() from @comms-id/abn/fixtures answers the same shapes with no network.

LIVE

Remove environment: "test" (LIVE is the default) and run with the production key. COMMS_ID_CLIENT_ID and COMMS_ID_PRIVATE_JWK come from your server’s secrets; nothing else changes.

Reference: ABN API. Every reply is one business, with source saying whether the live register or the dated snapshot answered; the snapshot answers when the register is down, and recheckAfter says when to ask again.

Was this page helpful?