DEV Community

Rostery Team
Rostery Team

Posted on

A Pre-Upload Check Workflow for NDIS Bulk Payment Requests: Five Gates, 16 Columns and the Codes Behind Rejected Lines (2026)

Originally based on our guide How to submit an NDIS bulk payment request without rejections on rostery.com.au. This version is rewritten for the people who build and run the claim file each week.

If you work in finance or operations at an NDIS provider, the bulk payment request is probably the most important CSV in your week. It is how a registered provider claims from the NDIA for many supports in one go, through the myplace provider portal. It is also unforgiving: one stray character can stop the whole file, and a clean file can still come back with lines that were never paid.

This post sets out a pre-upload workflow we'd suggest for admin teams: five gates a file passes through between the invoice run and the payment landing. Everything about the file format comes from the NDIA's Bulk payment requests: self-help guide for providers (December 2025). Where we say "the guide", that's the one we mean.

General information only, not claiming or legal advice. Check the current NDIA guidance before you change your process.

Five checks between the invoice run and the upload: template, fields, file, plan and price, and after upload

Why a gate model works for this file

A bulk payment request can fail in two places, and they behave very differently.

  1. At upload. The portal reads the file and refuses it if the header, file name, size or any field is wrong. Nothing is lodged. You get a message and, for row problems, an error file.
  2. After upload. Each row becomes its own payment request and is checked against the participant's plan, funding, provider records and the price limits. Some lines are paid, some are rejected with a code, and some are paid less than claimed.

The first three gates below deal with the first kind of failure. They are mechanical, so they suit a script or a checklist. The last two gates deal with the second kind, which needs information about the participant's plan that a script may not have.

Gate 1: start from a fresh template

Download the bulk upload template from the portal rather than copying last month's file. The guide says not to delete any columns, and a file with a missing or changed header row is refused outright. Save it as a comma-delimited CSV.

The template has 16 columns. Here is what goes in each, in plain terms:

The 16 columns of an NDIS bulk payment request: what goes in each and the format the NDIA asks for

A few notes the table can't hold:

  • RegistrationNumber is your Organisation ID from your portal profile. It starts with 405. NDISNumber starts with 43. Both are numbers only, nine digits.
  • ClaimReference is optional, but fill it. It's how you'll match a payment back to an invoice later. Keep it to 50 characters.
  • AuthorisedBy, ParticipantApproved and InKindFundingProgram are legacy fields. Leave them blank.
  • ABN of Support Provider is for plan-managed claims. It takes the provider's 11-digit ABN, or one of two exemption codes (REIMB or EXCLS). For a standard service booking you leave it blank.

Gate 2: field rules a script can test

Most refused files trace back to one field in one row. These rules are worth encoding:

  • Dates. The guide's error messages ask for YYYY-MM-DD. It also warns that Excel will reformat dates if you open and edit anything in the file, which is how a correct file goes wrong after a "quick look".
  • Quantity or Hours, never both. Fill one. If both are filled, the portal's message is "Both duration and quantity cannot co-exist". If neither is filled, that's an error too.
  • Hours go in as HHH:MM without leading zeros, so two and a half hours is 2:30.
  • UnitPrice is dollars and cents with no dollar sign.
  • GSTCode is P1, P2 or P5. Older habits such as typing "GST free" in words won't pass.
  • ClaimType is blank for a direct service, or one of CANC, REPW, TRAN, NF2F, THLT or IRSS, in capitals. CancellationReason (NSDH, NSDF, NSDT or NSDO) belongs only on CANC rows.

ABNs deserve their own check, because a typo produces an 11-digit number that looks fine. The Australian Business Register publishes the check digit method: subtract 1 from the first digit, multiply each digit by its weight, add them up, and the total must divide evenly by 89.

WEIGHTS = [10, 1, 3, 5, 7, 9, 11, 13, 15, 17, 19]

def abn_is_valid(abn: str) -> bool:
    digits = [int(c) for c in abn if c.isdigit()]
    if len(digits) != 11 or len(abn.replace(" ", "")) != 11:
        return False
    digits[0] -= 1
    return sum(d * w for d, w in zip(digits, WEIGHTS)) % 89 == 0

print(abn_is_valid("51 824 753 556"))  # True, the ABR's own worked example
Enter fullscreen mode Exit fullscreen mode

A passing check digit only means the number is well formed. It doesn't tell you the ABN is active, so plan-managed teams still need their usual ABN Lookup step.

Gate 3: file rules that refuse the whole upload

These are the rules that stop a file before any row is assessed:

  • The file name, including the extension, must be 20 characters or fewer. BULKUPLOAD2.CSV is 15.
  • No special characters in the name: \ ? / : * < > | or ".
  • Up to 5,000 rows, and the file must be under 10 MB.
  • Nothing in any cell beyond your data. The guide's example is claims ending on row 47 with a stray character in row 102, which stops the whole file loading.
  • A name you have never used before. A corrected file cannot reuse the name of any earlier upload, whether that upload worked or not.

Here's a compact preflight that runs gates 1 to 3 against a file and the fresh template you downloaded. It compares headers against the template rather than a hard-coded list, so it keeps working if the NDIA changes the template.

import csv, os, re

BAD_NAME = re.compile(r'[\\?/:*<>|"]')
HOURS = re.compile(r"^(0|[1-9]\d{0,2}):[0-5]\d$")
DATE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
CLAIM_TYPES = {"", "CANC", "REPW", "TRAN", "NF2F", "THLT", "IRSS"}
GST = {"P1", "P2", "P5"}

def preflight(path, template_path, used_names):
    problems = []
    name = os.path.basename(path)
    if len(name) > 20: problems.append("file name over 20 characters")
    if BAD_NAME.search(name): problems.append("special character in file name")
    if name.upper() in used_names: problems.append("file name already used")
    if os.path.getsize(path) >= 10 * 1024 * 1024: problems.append("file is 10 MB or more")

    with open(template_path, newline="") as t:
        expected = next(csv.reader(t))
    with open(path, newline="") as f:
        rows = list(csv.reader(f))
    if rows[0] != expected: problems.append("header differs from template")
    if len(rows) - 1 > 5000: problems.append("more than 5,000 rows")

    for n, r in enumerate(rows[1:], start=2):
        row = dict(zip(expected, r))
        if not any(v.strip() for v in r):
            problems.append(f"row {n}: blank row"); continue
        for col in ("SupportsDeliveredFrom", "SupportsDeliveredTo"):
            if not DATE.match(row.get(col, "")): problems.append(f"row {n}: {col} not YYYY-MM-DD")
        q, h = row.get("Quantity", "").strip(), row.get("Hours", "").strip()
        if bool(q) == bool(h): problems.append(f"row {n}: fill Quantity or Hours, not both or neither")
        if h and not HOURS.match(h): problems.append(f"row {n}: Hours should look like 2:30")
        if "$" in row.get("UnitPrice", ""): problems.append(f"row {n}: remove $ from UnitPrice")
        if row.get("GSTCode", "") not in GST: problems.append(f"row {n}: GSTCode must be P1, P2 or P5")
        if row.get("ClaimType", "") not in CLAIM_TYPES: problems.append(f"row {n}: unknown ClaimType")
    return problems
Enter fullscreen mode Exit fullscreen mode

Treat it as a starting point, not a complete validator. Column names are read from your own template, and you'll want to add the ABN check above and your own rules for item numbers.

Gate 4: what the NDIA checks after upload

Once the file is in, each line is checked against things that live in the NDIA's systems. The guide lists dozens of rejection codes. Grouped by where the fix usually is, they look like this:

What a rejected line is telling you to check: plan dates, funds, who may claim, item and price, duplicates and the claim time limit

You can't see a participant's remaining funds from your own system, but you can catch a fair share of these before upload:

  • Plan dates. If you hold the plan start and end dates, flag any line whose service dates fall outside them or straddle two plans (C22, C28, V26, V36). Split those lines by plan period.
  • Who may claim. For plans in the NDIA's new system, NDIA-managed and plan-managed supports need you recorded as a my provider. So do specialist disability accommodation, home and living and behaviour support claims. V11 is the code you'll see when you're not.
  • Item and price. Check that each item is valid on the service date (C16) and that the unit price is within the limit. The guide says the system only pays up to the maximum catalogue price, and marks the line Capped = YES in the reconciliation file. Code V31 also exists for a unit price above the limit, and C06 applies where the price is above an old-system service booking. Either way, the overage isn't paid. Our free NDIS price limit history is handy when a line straddles a price change.
  • Duplicates. Same participant, item and dates as an earlier claim means V04 or P04. A claim reference used more than twice gives P05.
  • Age. The NDIA rejects claims made more than two years after the support start date (C39). From December 2026, the NDIA says claims must be submitted within 90 days of delivering a support, so a "days since service" column in your check is worth adding now.

When a line does come back rejected, our free NDIA claim rejection codes lookup explains each code in plain English, with what to change before you claim again.

Gate 5: after upload, close the loop

From upload to a reconciled payment: submitted, check each line, fix and re-lodge only failed lines, paid, reconcile

  • Read every line's result. Each row is its own payment request, so record each outcome against its invoice.
  • Re-lodge only what failed. The guide is specific: save a new file with a new name, containing only the corrected records plus the header. Never reload the whole file, or you'll create duplicates of lines already paid.
  • Remember that Rejected is final for that request. A rejected payment request stays rejected. The fix is a new payment request.
  • Know the payment rhythm. Pending requests are paid after the next payment run, which happens at least once a day, Monday to Friday. Anything created after Friday's last run is picked up on the next business day.
  • Reconcile line by line. Compare the paid amount with the claimed amount, and look at Capped for lines paid short.

Making it a weekly routine

When Gate Who usually owns it
After invoices are approved 1 to 3, run the preflight Billing admin
Before upload 4, plan dates, my provider, prices, duplicates, age Billing admin, with the team lead for odd cases
Same or next business day 5, read results, re-lodge failed lines Billing admin
After the payment run 5, reconcile against invoices Finance

If you'd rather not maintain a script, our free NDIS bulk claim file checker runs the structural checks in your browser, and the file never leaves it. It checks every row and column against the NDIS Support Catalogue 2026-27, including dates Excel has rewritten, the hours format, the ABN check digit and prices above the national, remote and very remote limits. A clean result means the file is the right shape. The NDIA still checks the plan, bookings and funds.

For teams that would rather not build the file by hand at all, Rostery's end-to-end revenue engine takes a rostered shift through timesheet and invoice to the NDIS claim. If any of the terms here are new, our glossary entry on the NDIS bulk payment request is a short primer.

Earlier on dev.to we wrote about the other end of this chain: building an NDIS service agreement that holds up at claim time. Many of the lines that fail at gate 4 started there.

General information only. File rules and codes are from the NDIA's Bulk payment requests self-help guide (December 2025) and ndis.gov.au, read 26 October 2026. NDIA support for providers: 1300 311 675.

Top comments (0)