---
url: /software_partners/operate/respond_to_employer_issues.md
---

# Respond to employer issues

Some problems in the onboarding pipeline can't be fixed by SuperAPI alone - they need a human on the employer or partner side to act. An employee abandons onboarding before a default fund could be picked, a fund rejects a membership we thought was valid, an employer's stapling connection lapses.

An **employer issue** is a persistent, partner-actionable record describing one of these problems. Each issue gives you a stable id to use as an idempotency key, a status lifecycle so you know when to clear a notification, a `kind` describing what's wrong, and a list of `recommended_actions` you can surface to the employer. This guide covers how to listen for issues, fetch their detail, and drive them to resolution - including a couple of patterns for notifying employers (email, support tickets, or both).

## How an employer issue behaves

Before wiring anything up, it helps to understand the shape of an issue:

* **It belongs to an employer**, and may optionally also reference a single **employee**. Employer-only issues (no `employee_id`) describe a problem with the employer's configuration; employee-scoped issues describe a problem with one person's onboarding.
* **It has a `kind`** - a stable string identifying what's wrong. See [the kinds of issue](#the-kinds-of-issue) for a list of all issue kinds. Treat `kind` as an open set and handle unfamiliar values gracefully.
* **It has a status lifecycle**: an issue starts `open` and ends either `resolved` (someone or something fixed it) or `expired` (no one acted on it before its TTL elapsed, 90 days for most kinds). Both terminal states are final - an issue is never re-opened, and a new occurrence of the same problem opens a fresh issue with a new id.
* **Opening is idempotent.** SuperAPI keeps at most one open issue per problem, so you will not receive a flood of duplicates for the same underlying cause.

```mermaid
stateDiagram-v2
    [*] --> open: problem detected
    open --> resolved: auto-resolved or partner-resolved
    open --> expired: TTL elapses (90 days for most kinds)
    resolved --> [*]
    expired --> [*]
```

## The kinds of issue

Each `kind` describes a distinct problem and carries its own recommended actions.

| Kind | Meaning | Typical recommended actions |
| --- | --- | --- |
| `undefaulted_abandonment` | An employee abandoned onboarding without a default super fund being picked, because stapling wasn't enabled for the employer. | Re-onboard the employee to capture a real fund choice; enable stapling so future abandonments can be auto-defaulted. |
| `invalid_super_fund_membership` | A fund rejected a member verification request (MVR) for an APRA fund membership we believed was valid, i.e. the employee nominated a fund the fund says they don't belong to. | Confirm the employee's details (name, DOB, TFN, member number) match the fund's records. Re-register the employee with the fund if they are not yet a member. |
| `failed_fund_registration` | A member registration request to a super fund came back unsuccessful. The fund rejected the registration for any reason (a missing/closed account, a data mismatch, an employer-participation problem, etc.). | Re-onboard this employee through SuperAPI to capture a valid super fund choice. Follow up the rejection with the fund to confirm what is needed to register the member. |
| `employer_not_participating_in_fund` | A default-grow member registration was rejected by the fund because the employer is not set up as a participating employer for the fund they default employees into. This happens when an employer defaults an employee into a fund, but has no participating-employer arrangement with that fund, so the fund refuses to register the member. | Register this employer as a participating employer with the default fund so members can be registered to it. Review the employer's default super fund selection and choose a fund this employer has a participating-employer arrangement with. |
| `employer_requires_support_with_stapling` | The employer asked for help setting up super stapling from within the ATO settings embed. This is an employer-level issue with no `employee_id`, and it expires after 30 days rather than 90. The `context` carries the contact details and message the employer entered. | Retry the ATO connection setup instructions with the employer. A member of the SuperAPI support team will also reach out to assist. |
| `regulation_evidence_unobtainable` | The employee nominated a self-managed super fund, but Super Fund Lookup has no regulation evidence for it, so no Super Choice Form could be produced. The `context.reason` is `not_super_entity` or `not_a_pdf`. | Re-onboard the employee to capture a corrected self-managed super fund. |

We will roll out new kinds over time. Once employer issue webhooks are enabled, you automatically start receiving every kind we open for your employers - **you can't subscribe to a subset, and you can't influence which kinds we open or when**. That side of things is driven entirely by SuperAPI.

What you *do* control is your response. Whether a given issue triggers an email, a support ticket, both, or nothing at all is entirely your decision - opening an issue on our side carries no expectation that you act on it.

::: warning
**A new issue kind we ship should never break your integration.** Branch on the specific kinds you've built handling for, and let everything else fall through untouched until you've explicitly decided how to treat it. A new kind we ship should never break your integration or fire an unintended notification - receiving an unfamiliar `kind` is expected, and silently ignoring it is the correct behaviour until you choose to support it.
:::

When you are ready to handle a kind, the issue's `description` and `recommended_actions` are always populated regardless of kind, so you can surface useful guidance to the employer without hard-coding per-kind copy.

## Step 1 - Enable employer issue webhooks

Employer issue webhooks are **off by default**. Ask your SuperAPI contact to enable them for your partner account. Once enabled, issues are delivered to the same `webhook_url` you already use for the rest of your webhooks, signed with the same `webhook_signing_token` - there is no separate endpoint to configure.

::: info
If you aren't sure what your `webhook_url` is set to, you can retrieve your product settings using your [product API key](/software_partners/test/verify_your_product_api_key/index.html). See [Webhook security](/software_partners/build/webhooks/security/index.html) for verifying the signature, and [Work with webhooks locally](/software_partners/build/webhooks/work_locally/index.html) for testing against your machine.
:::

Two events are emitted under the `employer_issue` subject:

| Event | Fired when |
| --- | --- |
| `employer_issue_created` | A new issue has been opened for one of your employers. |
| `employer_issue_resolved` | An open issue was **automatically** resolved (e.g. the employee onboarded successfully and now has a valid fund membership). Not fired when you resolve an issue yourself - see [Resolving an issue](#step-4-resolve-an-issue). |

The payload follows the standard webhook shape:

```json
{
  "subject": "employer_issue",
  "event": "employer_issue_created",
  "id": "c2bc0c04-6b98-4333-91a6-919f5ebc9000",
  "remote_id": "c2bc0c04-6b98-4333-91a6-919f5ebc9000",
  "url": "https://api.superapi.com.au/api/v1/employer-issue/c2bc0c04-6b98-4333-91a6-919f5ebc9000",
  "product_id": "8f14e45f-ceea-467e-951e-93fa5ec8f4c0",
  "inserted_at": "2025-06-09T03:21:00Z",
  "version": "1"
}
```

The fields are the same as every SuperAPI webhook and are described in the [list of webhooks](/software_partners/build/webhooks/list_of_webhooks/index.html#the-payload). Two things are specific to this subject: `remote_id` mirrors `id`, because issues have no partner-supplied remote id, and `id` is the value to use as an idempotency key for any notification you raise.

## Step 2 - Fetch the issue

When you receive an `employer_issue_created` webhook, call the `url` from the payload to [fetch the issue](https://swagger.superapi.com.au/#tag/employer-issue/operation/SuperApiWeb.Api.V1.EmployerIssueController.show), authenticating with your product API key as you would any other request:

```bash
curl -X GET "https://api.superapi.com.au/api/v1/employer-issue/c2bc0c04-6b98-4333-91a6-919f5ebc9000" \
  -H "Content-Type: application/json" \
  -H "x-api-key: superapiproduct_yourkeyQvyaXOF"
```

A response looks like:

```json
{
  "version": "v1",
  "data": {
    "id": "c2bc0c04-6b98-4333-91a6-919f5ebc9000",
    "status": "open",
    "kind": "undefaulted_abandonment",
    "context": {
      "onboarding_session_id": "7d2f0c1e-4b8a-4c3d-9e2f-1a2b3c4d5e6f",
      "reason": "no_active_connection"
    },
    "description": "Onboarding session abandoned without a default super fund (stapling not enabled for this employer).",
    "employer_id": "9f1d8e2a-0b3c-4d5e-8f6a-7b8c9d0e1f2a",
    "employee_id": "2b3c4d5e-6f70-4a1b-9c2d-3e4f5a6b7c8d",
    "expires_at": "2025-09-07T03:21:00.000000Z",
    "resolved_at": null,
    "resolution_reason": null,
    "expired_at": null,
    "inserted_at": "2025-06-09T03:21:00.000000Z",
    "updated_at": "2025-06-09T03:21:00.000000Z",
    "recommended_actions": [
      {
        "code": "reonboard_employee",
        "description": "Re-onboard this employee through SuperAPI to capture a valid super fund choice."
      },
      {
        "code": "enable_stapling",
        "description": "Enable super stapling for this employer so future abandoned sessions can be auto-defaulted."
      }
    ]
  }
}
```

| Field | Description |
| --- | --- |
| `id` | The issue's primary key. |
| `status` | `open`, `resolved`, or `expired`. |
| `kind` | What's wrong, as a stable machine-readable string. See [The kinds of issue](#the-kinds-of-issue). |
| `context` | Extra detail whose shape depends on `kind`, for example the abandoned session id and the reason no default could be picked. The per-kind shapes are in the [API reference](https://api.superapi.com.au/swaggerui). |
| `description` | A human-readable summary of the problem, suitable to show directly to the employer. |
| `employer_id` | The employer this issue belongs to. Always present. |
| `employee_id` | The affected employee, or `null` for employer-level issues. |
| `expires_at` | When the issue auto-expires if no one acts on it. |
| `resolved_at` / `resolution_reason` | Populated once the issue is resolved. `resolution_reason` is one of `auto_resolved`, `partner_resolved`, `system_resolved`. |
| `expired_at` | Populated once the issue expires. |
| `recommended_actions` | An ordered list of `{code, description}` actions you can surface to the employer. The `code` is stable and machine-readable; the `description` is human-readable copy you can show as-is. |

::: tip
`recommended_actions` is computed per kind at read time, not stored. That means the guidance always reflects our current best advice for that kind of problem - you don't need to maintain your own copy of it, and you can render `description` directly in your UI or notification.
:::

### Listing open issues for an employer

Webhooks are the trigger to act in real time, but you can also reconcile state at any point - for example, to render an "issues" panel on an employer dashboard, or to catch up after downtime. [List issues for a specific employer](https://swagger.superapi.com.au/#tag/employer/operation/SuperApiWeb.Api.V1.EmployerIssueController.index), most-recent first:

```bash
curl -X GET "https://api.superapi.com.au/api/v1/employer/9f1d8e2a-0b3c-4d5e-8f6a-7b8c9d0e1f2a/employer-issues?status=open" \
  -H "Content-Type: application/json" \
  -H "x-api-key: superapiproduct_yourkeyQvyaXOF"
```

Pass `?status=open` to restrict to currently-open issues, or omit `status` to return every status. Results are paginated 50 per page via `?page=N`.

## Step 3 - Notify the employer

This is where you turn an issue into action on the employer's side. The right channel depends on your product, but two patterns cover most cases, and they compose well.

In both cases, **use the issue `id` as an idempotency key**. Issues open idempotently on our side - you will not get duplicate `employer_issue_created` webhooks for the same underlying problem - but keying your own side-effects on the issue id protects you against your own retries and redeliveries.

### Pattern A - Email the employer

Send the employer an email describing the problem and what to do about it. The `recommended_actions` descriptions are written to be shown directly, so you can render them as a checklist or a set of buttons that deep-link into the relevant part of your product (for example, a "Re-onboard this employee" button next to the `reonboard_employee` action).

```mermaid
sequenceDiagram
    participant S as SuperAPI
    participant P as Your system
    participant E as Employer

    S-->>P: Webhook: employer_issue_created
    P->>S: GET /employer-issue/{id}
    S-->>P: kind, recommended_actions, employee_id
    P->>P: Upsert notification keyed on issue id
    P->>E: Email: "Action needed for <employee>"
    Note over P,E: Employer acts (re-onboards, enables stapling…)
    S-->>P: Webhook: employer_issue_resolved
    P->>E: Mark notification resolved / send "all clear"
```

Keep a record linking the issue id to the email you sent, so that when the matching `employer_issue_resolved` webhook arrives you can mark the notification as cleared (and optionally send an "all clear" follow-up).

### Pattern B - Raise a ticket in the employer's support system

If your product integrates with a customer-service ticketing system (Zendesk, Freshdesk, an internal queue), an open issue maps naturally onto a ticket: create one when `employer_issue_created` fires, store the ticket reference against the issue id, and close the ticket when `employer_issue_resolved` fires.

The lifecycle alignment is the key benefit here - because each issue has exactly one open record on our side and a terminal resolved/expired state, you get a clean one-issue-to-one-ticket mapping without having to invent your own deduplication.

::: tip
The two patterns combine well. A common shape is: raise a ticket so your support team has a tracked work item, **and** email the employer so they know to act. Both are keyed on the same issue id, and both are cleared by the same `employer_issue_resolved` webhook.
:::

::: warning
Don't drop a notification just because an issue is also auto-resolvable. Many issues resolve themselves (see below), but only after the employer takes the very action you'd be notifying them about - re-onboarding the employee, enabling stapling. The notification is what prompts that action.
:::

## Step 4 - Resolve an issue

There are two ways an issue leaves the `open` state, and they behave differently with respect to webhooks.

**Auto-resolution.** Most issues resolve themselves once the underlying problem is genuinely fixed. SuperAPI re-checks open issues whenever relevant state changes - for example, when an employee completes a fresh onboarding session or gains a valid fund membership. If the world-state check for that kind now passes, the issue is closed with `resolution_reason: auto_resolved` and an `employer_issue_resolved` webhook fires. This is the path you should design for: notify on `created`, clear on `resolved`.

**Manual resolution.** If you've confirmed out-of-band that a problem is handled and want to clear it yourself, [resolve it via the API](https://swagger.superapi.com.au/#tag/employer-issue/operation/SuperApiWeb.Api.V1.EmployerIssueController.resolve):

```bash
curl -X PUT "https://api.superapi.com.au/api/v1/employer-issue/c2bc0c04-6b98-4333-91a6-919f5ebc9000/resolve" \
  -H "Content-Type: application/json" \
  -H "x-api-key: superapiproduct_yourkeyQvyaXOF"
```

This stamps `resolution_reason: partner_resolved` and returns the updated issue. It is idempotent - calling it against an already-resolved issue returns the current state - and it returns `422` if the issue has already expired, since that lifecycle is final.

::: warning
A manual resolve does **not** fire the `employer_issue_resolved` webhook. The reasoning: you just resolved it, so echoing a notification back to you would be redundant. If you resolve issues manually, clear your own notification in the same code path rather than waiting for a webhook that won't arrive.
:::

**Expiry.** An issue that no one acts on moves to `expired` after its TTL (90 days for most kinds, 30 for `employer_requires_support_with_stapling`). Expiry does not fire a webhook. If you keep long-lived notifications open, reconcile them periodically against the [list endpoint](#listing-open-issues-for-an-employer) so expired issues don't linger in your UI.

## Related reading

* [List of webhooks](/software_partners/build/webhooks/list_of_webhooks/index.html) for the full webhook reference, including the `employer_issue` subject.
* [Webhook security](/software_partners/build/webhooks/security/index.html) for verifying webhook signatures.
* [Work with webhooks locally](/software_partners/build/webhooks/work_locally/index.html) for testing webhooks against your machine.
* [Activate stapling](/software_partners/build/employers/activate_stapling/index.html) for the ATO setup steps that prevent most `undefaulted_abandonment` issues in the first place.
* [Lifecycle of an onboarding session](/software_partners/build/onboarding_sessions/lifecycle/index.html) for the abandonment decision flow that produces these issues.
