The same story repeats every month across hundreds of Saudi establishments: salaries were paid on time, the amounts were correct, the transfers landed in employee accounts — and the violation appears anyway. Compliance percentage drops, the Saudization certificate gets blocked, the visa request stalls.
At which point the search for a fix starts in the wrong place. The company hires a government-services office to upload the file on its behalf, or starts shopping for a new payroll system. Both treat the symptom.
The real cause is that the Wage Protection System does not measure whether you paid. It measures whether it could reconcile four separate data sources. If those sources disagree by a single character, the record is rejected regardless of the fact that the money arrived.
What Mudad's engine actually compares
Since the updates that came into effect through 2026, the linkage is close to real time: the bank notifies Mudad of the transfer, and Mudad compares it against the employee's contract in Qiwa and their registration in GOSI. You no longer upload a file and wait a month for the verdict — the discrepancy surfaces immediately on an inspector's dashboard.
Four comparisons run behind the scenes:
- Wage file against the Qiwa record — is every contracted employee present in the file, and does the amount match the contracted wage?
- Wage file against GOSI — an employee registered in social insurance but absent from the file reads as an unpaid wage.
- Wage file against the bank transfer — is the IBAN in the file the IBAN that actually received the money, and does the transferred value equal the declared value?
- Deductions against labour-law limits — any deduction exceeding statutory limits is flagged automatically, even if internally agreed.
Note that none of those four asks "did you pay?". All four ask "is your data consistent across four systems, none of which owns the others?"
Where rejected records are actually born
This is the list worth pinning up in front of the HR team, because every item on it originates inside your own systems, long before the file touches Mudad:
- A new hire in payroll not yet registered in GOSI — a two-day gap in the start date is enough.
- An Iqama or national ID entered with a trailing space, or in Arabic-Indic digits instead of Latin ones.
- A stale IBAN left in the employee record after they changed banks.
- A basic salary in your system that differs from the contracted wage after a promotion that was never updated in Qiwa.
- An employee on unpaid leave whose status was never updated, so the system reads a missing wage.
- A duplicate record produced by a mid-month correction run.
- Cash payment — always rejected. Mudad has stated officially that cash disbursement is not accepted when the file is submitted.
Every one of those is a two-record matching failure, not a payroll-software failure. Which is why replacing the payroll system rarely fixes it: the new system inherits the same inconsistent data.
The price is higher than the fine
The violations and penalties table published by the Ministry of Human Resources and Social Development sets a fine of SAR 10,000 for an employer who fails to upload the wage protection file monthly. Specialist market sources cite penalties starting around SAR 3,000 per employee for delay cases, with fines doubling on repeat offences.
But the fine is not the expensive line item. A falling compliance percentage leads to:
- The Saudization certificate being withheld — the entry ticket to government tenders.
- Suspension of visa issuance, contract authentication and sponsorship transfers.
- Establishment services suspended across Qiwa and Muqeem.
A contracting firm that loses tender eligibility because of a withheld Saudization certificate did not lose ten thousand riyals — it lost a project. That is the correct way to price the error.
On the compliance thresholds themselves, published figures circulate at 80%, 85% and 90% depending on establishment size and the service in question, and the statutory window to upload runs up to 30 days from the entitlement date, with a practical recommendation to submit before day 10. Always confirm the figures currently applying to your establishment directly with Mudad and MHRSD — they shift with each update wave. What does not shift is that the margin is narrow and getting narrower.
The fix: a reconciliation layer before upload
The correct intervention is neither in Mudad nor in the payroll system. It is in the gap between them. A small reconciliation layer that reads from your own sources and runs the same comparisons Mudad's engine will run — but before submission, while correction is still free.
type WageRecord = {
iqamaId: string;
fullNameAr: string;
iban: string;
basicWage: number;
housingAllowance: number;
otherAllowances: number;
deductions: number;
paidOn: string; // ISO date
};
type Finding = { iqamaId: string; code: string; detail: string };
const normalizeDigits = (s: string) =>
s
.replace(/[٠-٩]/g, d => String(d.charCodeAt(0) - 0x0660))
.replace(/\s+/g, "");
function reconcile(
payroll: WageRecord[],
qiwaContracts: Map<string, number>,
gosiRoster: Set<string>,
bankTransfers: Map<string, { iban: string; amount: number }>
): Finding[] {
const findings: Finding[] = [];
const seen = new Set<string>();
for (const row of payroll) {
const id = normalizeDigits(row.iqamaId);
if (seen.has(id)) {
findings.push({ iqamaId: id, code: "DUPLICATE", detail: "duplicate record" });
continue;
}
seen.add(id);
const contractWage = qiwaContracts.get(id);
const total = row.basicWage + row.housingAllowance + row.otherAllowances;
if (contractWage === undefined) {
findings.push({ iqamaId: id, code: "NO_CONTRACT", detail: "absent from Qiwa" });
} else if (total < contractWage * 0.8) {
findings.push({
iqamaId: id,
code: "WAGE_SHORTFALL",
detail: `transferred ${total} against contract ${contractWage}`,
});
}
const transfer = bankTransfers.get(id);
if (!transfer) {
findings.push({ iqamaId: id, code: "NO_TRANSFER", detail: "no bank transfer found" });
} else if (normalizeDigits(transfer.iban) !== normalizeDigits(row.iban)) {
findings.push({ iqamaId: id, code: "IBAN_MISMATCH", detail: "IBAN differs" });
}
}
// In GOSI but absent from the file — the most damaging rejection class
for (const id of gosiRoster) {
if (!seen.has(id)) {
findings.push({ iqamaId: id, code: "MISSING_RECORD", detail: "in GOSI, absent from file" });
}
}
return findings;
}The logic is deliberately simple. The value is not in sophistication, it is in timing: running these comparisons on the third of the month instead of waiting for a rejection notice turns a violation into a one-hour admin task.
On top of it you build a small dashboard: projected compliance percentage before upload, pending records ranked by severity, and a log of submitted justifications. That is precisely the reporting layer above existing systems — no payroll replacement, no outsourced office uploading on your behalf.
Two time windows worth knowing
- 72 hours to submit a pre-emptive justification when there is a technical fault, before the violation is formally recorded.
- 60 days to file an objection through Qiwa once a violation has been issued.
The first window is wasted almost every time, because nobody discovers the fault while it is still open. Automating detection is what makes the 72-hour window usable at all.
And as of 1 January 2026 the obligation extended to domestic workers via the Musaned platform, pulling establishments and individuals who paid a driver or house worker in cash into scope as well.
A pattern we have seen before
Anyone who read our guide to NPHIES claim denials in Saudi healthcare will recognise the architecture: a government platform enforcing agreement between systems never designed to talk to each other, and rejections that look random until their causes are measured. The same story played out in e-invoicing and ZATCA.
The conclusion holds in all three cases, and we laid it out in The ERP trap: integration, not replacement. Compliance in Saudi Arabia stopped being a question of which system to buy. It is now a question of how to make the systems you already run agree with each other before a regulator discovers that they do not.
Where to start next week
- Download the rejected-records report for the last three months from the compliance system, and sort it by rejection reason, not by employee.
- You will most likely find that two or three reasons account for the bulk of rejections. Those are the integration points worth fixing.
- Decide which system owns the truth for each field. Who owns the IBAN? Who owns the contracted wage? Who owns the start date? The absence of a clear answer is the root cause.
- Run reconciliation automatically before submission day, not after.
The first step is purely diagnostic. It needs no project and no budget — only that the rejection report gets read as data rather than as blame.
Are WPS violations recurring at your establishment despite salaries going out on time? Send us your rejected-records report for the last three months and we will return it classified by rejection cause, with the integration point responsible for each category identified. Free diagnostic, no commitment, via the contact page.
Sources and references: the violations and penalties table published by the Ministry of Human Resources and Social Development, Mudad Business documentation on the compliance system, and the GOSI e-services portal. Compliance percentages and deadlines change with each update wave — confirm the figures applying to your establishment with official sources before acting on them.