# 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`:

```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

```ts
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](/docs/reference/abn). 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.
