Numbers / Spending Methods
Ingest supplier bills as draft transactions in the Numbers module.
/v1/numbers-spending-methods
Posts a single spending method to the Numbers ledger. The result is a draft transaction (status='d') that surfaces in the existing Mosaic Hub UI (Numbers → Spending Methods) for review, commit, void and VAT processing.
For v1 the only supported type is supplier-bill. Future minor releases (still /v1/) will add more types and indicate them via additive bumps to schema_version.
Required scope
numbers.spending-methods.write
Request
Content-Type: multipart/form-data
| Part | Type | Required | Description |
|---|---|---|---|
spending-method | application/json (≤ 1 MB) | Yes | JSON body conforming to SpendingMethodRequest. |
attachment | application/pdf (≤ 25 MB) | No | Supporting document - stored against the resulting transaction. |
SpendingMethodRequest (supplier-bill)
{
"schema_version": "1.0.0",
"type": "supplier-bill",
"bill": {
"supplier_account_number": "ACME-001",
"invoice_number": "INV-2026-0042",
"invoice_date": "2026-05-28",
"due_date": "2026-06-27",
"currency_code": "ZAR",
"narration": "Office stationery - May 2026",
"lines": [
{ "description": "Paper, A4", "quantity": "5", "unit_amount": "100.00", "account_code": "5100", "vat201_rate_code": "20180401-15", "line_total": "500.00" },
{ "description": "Toner, black", "quantity": "1", "unit_amount": "750.00", "account_code": "5100", "vat201_rate_code": "20180401-15", "line_total": "750.00" }
],
"subtotal": "1250.00",
"vat_total": "187.50",
"total": "1437.50"
}
}
Validation rules applied server-side:
- All monetary amounts are decimal strings with up to 4 fractional digits.
currency_codeis required on every bill. Must be an ISO 4217 alpha-3 code in uppercase (ZAR,USD,EUR,GBP, ...). The value is persisted tonumbers_transaction_headers.transaction_currency; the bill, all line items and the Accounts Payable credit leg post in this single currency.line_totalis the VAT-exclusive (net) amount per line.- Sum of
line_totalmust equalsubtotal. subtotal + vat_totalmust equaltotal.supplier_account_numbermust match an active counterparty for the requesting client.vat201_rate_codeon every line must exist in the Mosaic VAT rate catalogue (see below). Inactive or out-of-window rates are accepted but raise a non-fatal warning in the response.schema_versionmajor version must be1.
VAT rate identifiers
Mosaic exposes a stable, human-meaningful vat201_rate_code in one of two shapes:
- SARS-aligned rates –
YYYYMMDD-<suffix>.YYYYMMDDis the rate's effective start date (e.g.20180401for the post-VAT-hike 15% era) or its end date for historical rates (e.g.20180331for the legacy 14% rates).<suffix>is the SARS VAT201 field number verbatim (1,1A,14,15A, ...). - Date-independent rates – a bare PascalCase token (
NotRegisteredVendor,OutOfScope,ZeroRatedPurchase) for non-VAT201 rows that have no SARS box.
| vat201_rate_code | Rate | VAT201 field | Description |
|---|---|---|---|
20180401-1 | 15% | 1 | Standard rate (excl. capital goods & accommodation) - output. |
20180401-1A | 15% | 1A | Standard rate, capital goods only - output. |
20180401-2 | 0% | 2 | Zero-rated supplies (local) - output. |
20180401-2A | 0% | 2A | Zero-rated supplies (exports) - output. |
20180401-3 | 0% | 3 | Exempt and non-taxable supplies - output. |
20180401-14 | 15% | 14 | Capital goods supplied to you (local) - input. |
20180401-14A | 15% | 14A | Capital goods imported by you - input. |
20180401-15 | 15% | 15 | Other goods/services supplied to you (local) - input. Most common bill line code. |
20180401-15A | 15% | 15A | Other goods imported by you - input. |
NotRegisteredVendor | 0% | n/a | Supplier is not a VAT vendor - no VAT charged. |
OutOfScope | 0% | n/a | Out of scope / non-VATable transaction. |
ZeroRatedPurchase | 0% | n/a | Zero-rated purchase. |
20180331-* | 14% | 1, 1A, 14, 15, ... | Historical (pre-1 April 2018) 14% rates. Accepted for back-dated imports - the API ingests but returns a vat-rate-inactive or vat-rate-out-of-window warning. |
The complete catalogue lives in numbers_value_added_tax_rates. Contact your Mosaic administrator if you need a code that is not in the table above.
Response
202 Accepted with a Location header.
{
"spending_method_identifier": "019f1234-5678-7abc-9def-0123456789ab",
"schema_version": "1.0.0",
"type": "supplier-bill",
"status": "draft",
"_links": {
"self": "/v1/numbers-spending-methods/019f1234-5678-7abc-9def-0123456789ab"
}
}
Warnings
When a bill is ingested but something looks off, the response includes a warnings array. The draft transaction is still created and surfaces in the Hub UI for review.
{
"spending_method_identifier": "019f1234-5678-7abc-9def-0123456789ab",
"schema_version": "1.0.0",
"type": "supplier-bill",
"status": "draft",
"warnings": [
{
"code": "vat-rate-out-of-window",
"message": "Invoice date 2018-02-15 falls outside the effective window of VAT rate 20180401-15; the bill was ingested but the transaction date and VAT period are inconsistent.",
"line_index": 0,
"vat201_rate_code": "20180401-15"
}
],
"_links": {
"self": "/v1/numbers-spending-methods/019f1234-5678-7abc-9def-0123456789ab"
}
}
| Warning code | Meaning |
|---|---|
vat-rate-inactive | The referenced vat201_rate_code exists but has active = false. Numbers will reject the commit until the line is revisited. |
vat-rate-out-of-window | The bill's invoice_date falls outside the rate's [start_date, end_date] window. Typical when posting back-dated imports against a current rate or vice versa. |
End-to-end cURL example
# Assumes you have already computed Authorization, X-Mosaic-Timestamp and X-Mosaic-Nonce # using the canonical string described in /mosaic-hub-developer-authentication.php curl -i -X POST https://api.mosaic.co.za/v1/numbers-spending-methods \ -H "Authorization: Mosaic-HMAC-SHA256 key-id=$KEY_ID,signature=$SIGNATURE" \ -H "X-Mosaic-Timestamp: $TIMESTAMP" \ -H "X-Mosaic-Nonce: $NONCE" \ -H "Idempotency-Key: $(uuidgen)" \ -F "spending-method=@bill.json;type=application/json" \ -F "attachment=@bill.pdf;type=application/pdf"
Idempotency
The Idempotency-Key header is mandatory. The server remembers the request hash (JSON + attachment) for at least 24 hours:
- Same key, same payload → the original 202 response is replayed verbatim with an additional
Idempotent-Replay: trueheader. No new draft transaction is created. - Same key, different payload →
422 idempotency-key-conflict.
Always generate a fresh UUID v7 per logical request, persist it locally before calling the API, and reuse it on retries.
Errors
| Status | Problem type | When it occurs |
|---|---|---|
| 400 | multipart-invalid | Could not parse the multipart request. |
| 400 | spending-method-missing | The spending-method part is missing. |
| 400 | spending-method-invalid-json | The part is not valid JSON. |
| 400 | idempotency-key-missing | The Idempotency-Key header is missing or too long. |
| 413 | payload-too-large | JSON part exceeds 1 MB or attachment exceeds 25 MB. |
| 415 | unsupported-media-type | JSON part is not application/json or attachment is not application/pdf. |
| 422 | schema-validation-failed | Body failed JSON Schema validation; errors array contains AJV diagnostics. |
| 422 | schema-version-unsupported | schema_version major is not 1. |
| 422 | type-unsupported | type is not supplier-bill. |
| 422 | balance-mismatch | Line totals, subtotal or grand total do not reconcile. |
| 422 | supplier-not-found | No active counterparty matches supplier_account_number. |
| 422 | vat-rate-unknown | One or more lines reference a vat201_rate_code that is not in the Mosaic VAT rate catalogue. |
| 422 | idempotency-key-conflict | Idempotency-Key reused with a different payload. |