Skip to content

Make the QR code in your system

This is the recommended integration for Australian and New Zealand v2 payslips. The SDK validates the payslip and encrypts the PII in your system. You send the non-PII data and the encryption metadata to Verifiabl. You make the QR code in your system.

Cannot make the QR code in your system? Verifiabl can return a ready-made PNG. That integration sends the encrypted PII to Verifiabl. Refer to Get the QR code from Verifiabl.

To call the HTTP API without the SDK, follow Use the Issuer API. For client options, retries and errors, refer to the Issuer SDK guide.

npm install @verifiabl/issuer@1.0.0
dotnet add package Verifiabl.Issuer --version 1.0.0
gem install verifiabl-issuer --version 1.0.0

Refer to the complete example on GitHub.

Refer to the complete example on GitHub.

Ruby requires version 3.3 or newer. Refer to the complete example on GitHub.

Create a client with your client ID and client secret. The SDK gets an access token and renews it.

import { VerifiablClient } from "@verifiabl/issuer";

const clientId = process.env.VERIFIABL_CLIENT_ID?.trim();
const clientSecret = process.env.VERIFIABL_CLIENT_SECRET?.trim();

if (!clientId || !clientSecret) {
  throw new Error("Missing VERIFIABL_CLIENT_ID or VERIFIABL_CLIENT_SECRET environment variable");
}

const client = new VerifiablClient({
  environment: "sandbox",
  auth: { clientId, clientSecret },
});
using Verifiabl;
using Verifiabl.Client;

string clientId = Environment.GetEnvironmentVariable("VERIFIABL_CLIENT_ID")?.Trim()
    ?? throw new InvalidOperationException("Missing VERIFIABL_CLIENT_ID environment variable");
string clientSecret = Environment.GetEnvironmentVariable("VERIFIABL_CLIENT_SECRET")?.Trim()
    ?? throw new InvalidOperationException("Missing VERIFIABL_CLIENT_SECRET environment variable");

if (clientId.Length == 0 || clientSecret.Length == 0)
{
    throw new InvalidOperationException("Missing VERIFIABL_CLIENT_ID or VERIFIABL_CLIENT_SECRET environment variable");
}

var client = new VerifiablClient(new VerifiablClientOptions
{
    Environment = VerifiablEnvironment.Sandbox,
    Auth = VerifiablAuth.ClientCredentials(clientId, clientSecret),
});
require "verifiabl/issuer"

issuer = Verifiabl::Issuer::Client.new(
  Verifiabl::Issuer::Configuration.new(
    environment: :sandbox, # or :production
    client_id: ENV.fetch("VERIFIABL_CLIENT_ID"),
    client_secret: ENV.fetch("VERIFIABL_CLIENT_SECRET")
  )
)

Prepare the payslip with the helper for its country. The helper validates the non-PII data and encrypts the PII with your encryption key. Use the prepared payslip in the next steps.

import { prepareAustralianV2Payslip } from "@verifiabl/issuer";

const encodedKey = process.env.VERIFIABL_ENCRYPTION_KEY_BASE64;
if (!encodedKey) throw new Error("Missing VERIFIABL_ENCRYPTION_KEY_BASE64 environment variable");
const key = Buffer.from(encodedKey, "base64");

const prepared = prepareAustralianV2Payslip({
  pii: {
    employeeName: "Ana Leota", employerName: "Example Software Pty Ltd",
    employerAbn: "12 345 678 901",
    address: { lines: ["12 Example St"], suburb: "Sydney", stateOrTerritory: "NSW", postcode: "2000" },
  },
  payslipNonPii: {
    periodEnd: "2026-08-31", paymentDate: "2026-09-04", currency: "AUD",
    gross: "9000.00", paygw: "2250.00", net: "6750.00",
  },
  issuedAt: new Date().toISOString(), key,
});
using Verifiabl;
using Verifiabl.Client;

byte[] key = Convert.FromBase64String(
    Environment.GetEnvironmentVariable("VERIFIABL_ENCRYPTION_KEY_BASE64")!);

PreparedV2Payslip prepared = V2Issuance.PrepareAustralian(
    pii: new AustralianPiiFields
    {
        EmployeeName = "Ana Leota", EmployerName = "Example Software Pty Ltd",
        EmployerAbn = "12 345 678 901",
    },
    payslip: new AustralianPayslipV2
    {
        PeriodEnd = new DateOnly(2026, 8, 31), PaymentDate = new DateOnly(2026, 9, 4),
        Currency = PayslipCurrencies.Aud,
        Gross = 9000.00m, Paygw = 2250.00m, Net = 6750.00m,
    },
    issuedAt: DateTimeOffset.UtcNow, key: key);
require "base64"

key = Base64.strict_decode64(ENV.fetch("VERIFIABL_ENCRYPTION_KEY_BASE64"))

prepared = Verifiabl::Issuer.prepare_australian_v2_payslip(
  pii: {
    employee_name: "Ana Leota", employer_name: "Example Software Pty Ltd",
    employer_abn: "12 345 678 901",
    address: {lines: ["12 Example St"], suburb: "Sydney", state_or_territory: "NSW", postcode: "2000"}
  },
  payslip_non_pii: {
    period_end: "2026-08-31", payment_date: "2026-09-04", currency: "AUD",
    gross: "9000.00", paygw: "2250.00", net: "6750.00"
  },
  issued_at: Time.now.utc, key: key
)

The example uses DateOnly on .NET 8 or newer. On .NET Framework 4.7.2, use YYYY-MM-DD strings for payslip dates.

Send the prepared registration to registerNonPII. The API returns the reference for the payslip. Use it to make the QR code in step 4.

const result = await client.registerNonPii(prepared.registration);
RegisterNonPiiResponse result = await client.RegisterNonPiiAsync(prepared.Registration);
result = issuer.register_non_pii(**prepared.registration)

The SDK retries registration with the same reference after a temporary failure. By default, it makes 2 retries, and the call times out after 30 seconds. Refer to Retries and timeouts.

Make the QR code from the prepared payslip and the reference from step 3. If registration fails after the retries, do not make a QR code for the payslip. To register it again later, refer to Register again later.

import { createBarcodeSvg } from "@verifiabl/issuer";

const parts = prepared.barcodeParts(result.verifiablReference);
const badge = createBarcodeSvg(parts, { environment: "sandbox" });
const svg = badge.svg;
BarcodeParts parts = prepared.BarcodeParts(result.VerifiablReference);
BarcodeSvgResult badge = VerifiablBarcode.CreateSvg(
    parts, new BarcodeSvgOptions { Environment = VerifiablEnvironment.Sandbox });
string svg = badge.Svg;
parts = prepared.barcode_parts(result.verifiabl_reference)
barcode = Verifiabl::Issuer.build_barcode_svg(**parts, environment: :sandbox)
svg = barcode.svg

The QR code holds a scan URL: https://v.verifiabl.io/v/<verifiablReference>#2.<BASE32>. Set environmentEnvironmentenvironment: to the same environment as your client. In sandbox, the host is v.sandbox.verifiabl.io.

Use the SVG if your PDF library accepts vector images. If it does not, use the PNG. For the options, refer to Make QR codes.

Put the QR code on the payslip PDF, usually in the bottom-right corner.

Keep a light margin around the QR code. Scanners need this margin. The image has a margin only on the side next to the “Secured by Verifiabl” frame. On the other sides, make the margin at least one tenth of the QR code width:

  • For the vertical layout, these sides are the left, the right and the bottom.
  • For the horizontal layout, these sides are the left, the top and the bottom.

One tenth of the QR code width is more than four QR modules (the small squares) for all payslips. Four modules is the minimum.

Add the payload 2|<verifiablReference>|<BASE32> to the XMP metadata of the PDF. A verifier’s system reads the metadata copy first, because it does not need to decode the QR code. A copy made with print to PDF, or a photograph of the payslip, does not keep the metadata. These copies need the QR code.

import { buildBarcodePayload } from "@verifiabl/issuer";

const xmpPayload = buildBarcodePayload(parts);
string xmpPayload = VerifiablBarcode.BuildPayload(parts);
xmp_payload = Verifiabl::Issuer.build_barcode_payload(
  **parts
)

For the XMP namespace and property name, refer to PDF metadata copy.

Batch registration sends up to 1,000 records in one request. Single registration sends one record in each request. Use batch registration for a pay run. Prepare each payslip, then register the records with registerNonPIIBatch. The results are in the same order as the records. A record that is not valid does not stop the other records. To identify a record in the results, send externalIdExternalIdexternal_id. For each created or duplicate result, make the QR code from the prepared payslip for that record. Do not make a QR code for an error result. To register it again later, refer to Register again later.

import { createBarcodeSvg, prepareAustralianV2Payslip } from "@verifiabl/issuer";

const issuedAt = new Date().toISOString();
const batchPrepared = payslips.map((payslip) =>
  prepareAustralianV2Payslip({ pii: payslip.pii, payslipNonPii: payslip.nonPii, issuedAt, key }),
);
const batchResponse = await client.registerNonPiiBatch({
  records: batchPrepared.map((item, i) => ({
    ...item.registration,
    verifiablReference: item.verifiablReference,
    externalId: payslips[i].externalId,
  })),
});
const qrSvgs = new Map<string, string>();
batchResponse.results.forEach((result, i) => {
  if (result.status === "created" || result.status === "duplicate") {
    const parts = batchPrepared[i].barcodeParts(result.verifiablReference);
    qrSvgs.set(result.verifiablReference, createBarcodeSvg(parts, { environment: "sandbox" }).svg);
  } else {
    // Handle result.code. Do not parse result.detail.
  }
});
DateTimeOffset issuedAt = DateTimeOffset.UtcNow;
var batchPrepared = payslips.Select(payslip =>
    V2Issuance.PrepareAustralian(payslip.Pii, payslip.Payslip, issuedAt, key)).ToList();
RegisterNonPiiBatchResponse batchResponse = await client.RegisterNonPiiBatchAsync(
    batchPrepared.Select((item, index) => new BatchRecord
    {
        VerifiablReference = item.VerifiablReference,
        Schema = item.Registration.Schema,
        IssuedAt = item.Registration.IssuedAt,
        PayslipNonPii = item.Registration.PayslipNonPii,
        EncryptionMetadata = item.Registration.EncryptionMetadata,
        ExternalId = payslips[index].ExternalId,
    }).ToList());
var qrSvgs = new Dictionary<string, string>();
for (int i = 0; i < batchResponse.Results.Count; i++)
{
    BatchRecordResult recordResult = batchResponse.Results[i];
    if (recordResult.Status == BatchRecordStatuses.Created
        || recordResult.Status == BatchRecordStatuses.Duplicate)
    {
        BarcodeParts recordParts = batchPrepared[i].BarcodeParts(recordResult.VerifiablReference);
        qrSvgs[recordResult.VerifiablReference] = VerifiablBarcode.CreateSvg(
            recordParts, new BarcodeSvgOptions { Environment = VerifiablEnvironment.Sandbox }).Svg;
    }
    else { /* Handle recordResult.Code. Do not parse recordResult.Detail. */ }
}
batch_prepared = payslips.map do |payslip|
  Verifiabl::Issuer.prepare_australian_v2_payslip(pii: payslip.fetch(:pii),
    payslip_non_pii: payslip.fetch(:non_pii), issued_at: Time.now.utc, key: key)
end
records = batch_prepared.each_with_index.map do |item, index|
  item.registration.merge(external_id: payslips.fetch(index).fetch(:external_id))
end
batch = issuer.register_non_pii_batch(records)
qr_svgs = {}
batch.results.each_with_index do |result, index|
  if %w[created duplicate].include?(result.status)
    parts = batch_prepared.fetch(index).barcode_parts(result.verifiabl_reference)
    qr_svgs[result.verifiabl_reference] =
      Verifiabl::Issuer.build_barcode_svg(**parts, environment: :sandbox).svg
  else
    # Handle result.code. Do not parse result.detail.
  end
end

For the result statuses, refer to Register a pay run.