Overview
A Pay Event represents one pay run — a container for the individual employee pay items in that run. The STP lifecycle is:
draft → ready → submitted → accepted (or rejected / error)
- Create a pay event in
draftstatus - Add items (one per employee)
- Mark ready
- Submit to the ATO
- Poll or wait for webhook to know when it's accepted
The Pay Event object
Common fields
| Field | Type | Notes |
|---|---|---|
id |
integer | |
employer_id |
integer | |
employer_name |
string | Computed |
pay_period_start |
string (ISO 8601 date) | |
pay_period_end |
string (ISO 8601 date) | |
period_label |
string | Human-readable range |
payment_date |
string (ISO 8601 date) | The actual pay date |
pay_frequency |
string | weekly, fortnightly, monthly, quarterly |
event_type |
string | pay_event, update_event, full_file_replacement |
status |
string | draft, ready, submitted, accepted, rejected, error |
employee_count |
integer | Number of items on this pay event |
total_gross |
number | Computed. Sum of item gross amounts |
total_tax |
number | Computed. Sum of item PAYG withholding |
total_super |
number | Computed. Sum of item super guarantee |
is_finalisation |
boolean | True if this is an EOFY finalisation event |
financial_year |
string | e.g. 2026-27, set automatically from payment_date if blank |
created_at |
string (ISO 8601) | |
updated_at |
string (ISO 8601) |
Detail fields (on GET /:id)
| Field | Type | Notes |
|---|---|---|
notes |
string | Free-text notes |
submitted_at |
string (ISO 8601) | |
responded_at |
string (ISO 8601) | When ATO replied |
message_id |
string | ATO message ID once submitted |
items |
array | Pay event items (see below) |
Endpoints
List pay events
GET /api/v1/pay_events
Scopes: pay_events:read
Query parameters:
| Param | Default | Notes |
|---|---|---|
employer_id |
— | Filter by employer |
status |
— | Filter by status |
page / per_page |
1 / 25 |
Response: 200 OK with an array of compact Pay Event objects.
Get a pay event (with items)
GET /api/v1/pay_events/:id
Scopes: pay_events:read
Response: 200 OK with full detail including items array.
Create a pay event
POST /api/v1/pay_events
Scopes: pay_events:write
Idempotent: yes
Body:
{
"pay_event": {
"employer_id": 12,
"pay_period_start": "2026-04-01",
"pay_period_end": "2026-04-14",
"payment_date": "2026-04-14",
"pay_frequency": "fortnightly",
"event_type": "pay_event",
"is_finalisation": false,
"notes": "Fortnight 1 for FY 26-27"
}
}
Required: employer_id, pay_period_start, pay_period_end, payment_date
Response: 201 Created with the new Pay Event (empty items).
Update a pay event
PATCH /api/v1/pay_events/:id
Scopes: pay_events:write
Idempotent: yes
Only allowed while the pay event is in draft status. Once it's ready or later, updates return 403 Forbidden.
Pay event items
Each item represents one employee's pay data for this pay event. STP Phase 2 requires disaggregated reporting — you send separate amounts for ordinary time earnings, overtime, allowances, etc.
Item fields
All monetary fields are decimal AUD amounts, defaulting to 0.00. Required for every item: employee_id.
| Field | Type | Notes |
|---|---|---|
id |
integer | |
employee_id |
integer | Required |
employee_name |
string | Computed |
employee_payroll_id |
string | Computed |
hours_paid |
number | Total hours paid in this period |
gross_payment |
number | Total gross |
ordinary_time_earnings |
number | OTE component |
overtime |
number | |
bonuses_commissions |
number | |
directors_fees |
number | |
salary_sacrifice_super |
number | Salary sacrificed into super |
salary_sacrifice_other |
number | Other salary sacrifice |
task_allowances |
number | |
lump_sum_a |
number | Long service leave, annual leave on termination |
lump_sum_b |
number | Unused long service leave from pre-16 Aug 1978 |
lump_sum_d |
number | Tax-free component of genuine redundancy |
lump_sum_e |
number | Back payment accruing ≥12 months |
lump_sum_w |
number | Return to work payments |
payg_withholding |
number | Tax withheld |
foreign_tax_paid |
number | |
super_guarantee |
number | SG contribution |
super_salary_sacrifice |
number | |
employer_additional_super |
number | Above-SG employer contributions |
reportable_super |
number | Reportable employer super contributions (RESC) |
qualifying_earnings |
number | Payday Super (from 1 Jul 2026). SG contribution base per QE rules. Leave as 0 for pre-Payday-Super pay events. |
annual_leave |
number | Annual leave paid |
personal_leave |
number | Personal/sick leave paid |
long_service_leave |
number | |
community_service_leave |
number | |
paid_leave |
number | Write-only convenience alias. If your source system doesn't yet disaggregate leave by type, send one flat paid_leave figure and we'll book it as annual_leave on ingestion. Ignored if any of the four STP Phase 2 sub-types — annual_leave, personal_leave, long_service_leave, community_service_leave — is also supplied. Not returned on reads — the stored value is in annual_leave. See Leave disaggregation below. |
etp_taxable |
number | Employment termination payment — taxable |
etp_tax_free |
number | ETP — tax-free |
etp_tax_withheld |
number | ETP — tax withheld |
etp_code |
string | ETP reason code |
rfba_exempt |
number | Reportable fringe benefits — exempt |
rfba_non_exempt |
number | Reportable fringe benefits — non-exempt |
total_super |
number | Computed. Sum of all super components |
total_leave |
number | Computed. Sum of all leave components |
Add an item
POST /api/v1/pay_events/:id/items
Scopes: pay_events:write
Idempotent: yes
Body:
{
"item": {
"employee_id": 88,
"hours_paid": 76.0,
"gross_payment": 4820.00,
"ordinary_time_earnings": 4820.00,
"payg_withholding": 1145.00,
"super_guarantee": 554.30
}
}
Response: 201 Created with the new Item.
Only one item per employee per pay event. POSTing a second item for the same employee returns 422 Unprocessable Entity.
Update an item
PATCH /api/v1/pay_events/:id/items/:item_id
Scopes: pay_events:write
Body: same shape as create. employee_id cannot be changed — if you need a different employee, delete and re-add.
Remove an item
DELETE /api/v1/pay_events/:id/items/:item_id
Scopes: pay_events:write
Response: 200 OK with { "message": "... removed from pay event" }.
Lifecycle actions
Mark ready
Transitions the pay event from draft → ready. No more items can be added, edited, or removed.
POST /api/v1/pay_events/:id/mark_ready
Scopes: pay_events:write
Idempotent: yes
Preconditions:
- Must be in draft status
- Must have at least one item
Response: 200 OK with updated Pay Event.
Errors:
- 400 Bad Request if the pay event is not draft, or has no items
Submit to ATO
Queues the pay event for asynchronous lodgement to the ATO gateway.
POST /api/v1/pay_events/:id/submit
Scopes: pay_events:write
Idempotent: yes
Preconditions:
- Must be in ready status
- Employer must have valid M2M credentials (for live; test uses Beeswax's shared credentials)
Response: 200 OK with updated Pay Event in submitted status.
Important: This returns immediately — the actual ATO submission happens in a background job. The pay event status will change from submitted to accepted / rejected / error when the ATO responds. Use webhooks to be notified when this happens, or poll /api/v1/submissions?employer_id=X.
Pay event status lifecycle
draft ─────────────► ready ─────────────► submitted ─────┬─► accepted
├─► rejected
└─► error
| Status | Meaning |
|---|---|
draft |
Created, being built up with items. Fully editable. |
ready |
Marked ready. No more edits. Queued locally but not yet sent. |
submitted |
Dispatched to the background lodgement job |
accepted |
ATO confirmed receipt and validation passed |
rejected |
ATO validation failed — check the linked submission for error details |
error |
Something went wrong in our infrastructure (signing, network, etc.) — usually retryable |
Error handling
If a submission fails, check the linked Submission for the full ATO response and error messages.
You can retry a failed pay event by:
- Creating a new pay event for the same period
- Re-adding the items
- Marking ready and submitting
The ATO tracks uniqueness by message_id, which Beeswax generates automatically — retries after a failure get a fresh message ID so the ATO treats them as new.
For editing a successful submission, see the STP Phase 2 update_event and full_file_replacement event types — set event_type on create to target those modes.
Common tasks
Lodge a weekly pay event for 5 employees
# 1. Create the pay event
PAY_EVENT_ID=$(curl -s https://stp.beeswaxapp.com/api/v1/pay_events \
-X POST \
-H "Authorization: Bearer stp_test_..." \
-H "Idempotency-Key: payrun-acme-2026-04-14" \
-H "Content-Type: application/json" \
-d '{
"pay_event": {
"employer_id": 12,
"pay_period_start": "2026-04-08",
"pay_period_end": "2026-04-14",
"payment_date": "2026-04-14",
"pay_frequency": "weekly"
}
}' | jq -r '.id')
# 2. Add items for each employee
for emp in 88 89 90 91 92; do
curl -s https://stp.beeswaxapp.com/api/v1/pay_events/$PAY_EVENT_ID/items \
-X POST \
-H "Authorization: Bearer stp_test_..." \
-H "Idempotency-Key: item-$PAY_EVENT_ID-$emp" \
-H "Content-Type: application/json" \
-d "{ \"item\": { \"employee_id\": $emp, \"gross_payment\": 1000.00, \"payg_withholding\": 200.00, \"super_guarantee\": 115.00 } }"
done
# 3. Mark ready
curl -s https://stp.beeswaxapp.com/api/v1/pay_events/$PAY_EVENT_ID/mark_ready \
-X POST \
-H "Authorization: Bearer stp_test_..."
# 4. Submit
curl -s https://stp.beeswaxapp.com/api/v1/pay_events/$PAY_EVENT_ID/submit \
-X POST \
-H "Authorization: Bearer stp_test_..."
Leave disaggregation
STP Phase 2 requires leave to be reported in four buckets, not a single figure:
annual_leavepersonal_leave(sick, carer's, compassionate)long_service_leavecommunity_service_leave(jury duty, SES, RFS)
If your source system already tracks these separately, send them directly and the values flow through to the ATO unchanged.
If your source system only tracks one "paid leave" number (common for smaller payroll products and for one-off manual pay runs), send that number in paid_leave on the item payload. We map it to annual_leave on ingestion, because annual leave is the dominant category for SMB payroll — roughly 70% of all paid leave taken nationally, and LSL only kicks in at 7–10 years of continuous service, which is comparatively rare.
Rules:
- If any of the four sub-types is explicitly present and non-blank,
paid_leaveis ignored — the explicit value wins. This lets you mix them: sendpaid_leave: 200, personal_leave: 50and only the personal leave is recorded (200 is discarded because a sub-type was supplied). paid_leaveis write-only. OnGET, the stored value appears asannual_leave;paid_leaveis not returned.- If disaggregated reporting matters for your product (e.g. you surface leave balances to employees by type), use the sub-type fields — the fallback is a transitional convenience, not the recommended long-term shape.