# Stairling - Guide

Complete walkthrough to run a payroll calculation through the Verso Partner API: create an employee, inject payroll variables, execute the calculation, and retrieve results.

## Prerequisites

Before starting, ensure you have:

- A **partner API key** (provided by Verso)
- Your **partner ID** and **tenant ID**
- The tenant must have the **StairlingPayroll** regulation imported

All examples use these variables:

```bash
GW="https://your-gateway-url"
KEY="your-partner-api-key"
PARTNER_ID=1
TENANT_ID=1
PAYROLL_ID=1
```

All endpoints follow the pattern: `GET/POST /partners/{partnerId}/tenants/{tenantId}/...`

## Step 1 — Discover resources

Verify connectivity and retrieve IDs for your tenant.

```bash title="GET /partners/{partnerId}/tenants/{tenantId}/divisions"
curl -s "$GW/partners/$PARTNER_ID/tenants/$TENANT_ID/divisions" \
  -H "Authorization: Bearer $KEY"
```

```bash title="GET /partners/{partnerId}/tenants/{tenantId}/payrolls"
curl -s "$GW/partners/$PARTNER_ID/tenants/$TENANT_ID/payrolls" \
  -H "Authorization: Bearer $KEY"
```

```bash title="GET /partners/{partnerId}/tenants/{tenantId}/employees"
curl -s "$GW/partners/$PARTNER_ID/tenants/$TENANT_ID/employees" \
  -H "Authorization: Bearer $KEY"
```

## Step 2 — Create an employee

```json title="POST /partners/{partnerId}/tenants/{tenantId}/employees"
{
  "identifier": "pierre.martin@example.com",
  "firstName": "Pierre",
  "lastName": "Martin",
  "culture": "fr-FR",
  "divisions": ["StairlingVTC"]
}
```

**Response** (201 Created):

```json
{
  "identifier": "pierre.martin@example.com",
  "firstName": "Pierre",
  "lastName": "Martin",
  "divisions": ["StairlingVTC"],
  "id": 23,
  "status": "Active"
}
```

Capture the `id` field — you will need it for case injection and payrun.

## Step 3 — Inject case values

All cases are injected via `POST /partners/{partnerId}/tenants/{tenantId}/payrolls/{payrollId}/cases` using the `CaseChangeSetup` payload format.

### Contrat

```json title="POST /payrolls/{payrollId}/cases"
{
  "userId": 1,
  "employeeId": 23,
  "divisionId": 1,
  "reason": "Init Contrat",
  "case": {
    "caseName": "Contrat",
    "values": [
      {
        "caseFieldName": "DateDebutContrat",
        "value": "2025-01-01",
        "start": "2025-01-01T00:00:00Z"
      }
    ]
  }
}
```

### Remuneration

```json title="POST /payrolls/{payrollId}/cases"
{
  "userId": 1,
  "employeeId": 23,
  "divisionId": 1,
  "reason": "Remuneration Janvier 2025",
  "case": {
    "caseName": "Remuneration",
    "values": [
      { "caseFieldName": "HeuresTravaillees", "value": "151.67", "start": "2025-01-01T00:00:00Z" },
      { "caseFieldName": "TauxHoraire", "value": "13.00", "start": "2025-01-01T00:00:00Z" }
    ]
  }
}
```

### Frais

```json title="POST /payrolls/{payrollId}/cases"
{
  "userId": 1,
  "employeeId": 23,
  "divisionId": 1,
  "reason": "Frais Janvier 2025",
  "case": {
    "caseName": "Frais",
    "values": [
      { "caseFieldName": "NombreRepas", "value": "20", "start": "2025-01-01T00:00:00Z" },
      { "caseFieldName": "MontantRepas", "value": "20.70", "start": "2025-01-01T00:00:00Z" }
    ]
  }
}
```

### Prevoyance & Mutuelle

```json title="POST /payrolls/{payrollId}/cases"
{
  "userId": 1,
  "employeeId": 23,
  "divisionId": 1,
  "reason": "Prevoyance Mutuelle",
  "case": {
    "caseName": "PrevoyanceMutuelle",
    "values": [
      { "caseFieldName": "HasPrevoyance", "value": "true", "start": "2025-01-01T00:00:00Z" },
      { "caseFieldName": "TauxPrevoyance", "value": "0.0089", "start": "2025-01-01T00:00:00Z" },
      { "caseFieldName": "HasMutuelleObligatoire", "value": "true", "start": "2025-01-01T00:00:00Z" },
      { "caseFieldName": "HasMutuelleFacultative", "value": "false", "start": "2025-01-01T00:00:00Z" }
    ]
  }
}
```

### PAS (Prelevement a la Source)

```json title="POST /payrolls/{payrollId}/cases"
{
  "userId": 1,
  "employeeId": 23,
  "divisionId": 1,
  "reason": "PAS Janvier 2025",
  "case": {
    "caseName": "PAS",
    "values": [
      { "caseFieldName": "TauxPAS", "value": "0.05", "start": "2025-01-01T00:00:00Z" }
    ]
  }
}
```

### Verify injected values

```bash title="GET /employees/{employeeId}/cases"
curl -s "$GW/partners/$PARTNER_ID/tenants/$TENANT_ID/employees/23/cases" \
  -H "Authorization: Bearer $KEY"
```

You should see all 10 case field values (Contrat, Remuneration, Frais, PrevoyanceMutuelle, PAS).

## Step 4 — Run payroll

### Start the payrun job

```json title="POST /partners/{partnerId}/tenants/{tenantId}/payruns/jobs"
{
  "name": "Paie_Janvier_2025",
  "payrunName": "Monthly",
  "userIdentifier": "support@stairling.coop",
  "periodStart": "2025-01-01T00:00:00Z",
  "reason": "Paie mensuelle Janvier 2025",
  "forecast": "demo-jan2025-unique-id",
  "employeeIdentifiers": ["pierre.martin@example.com"]
}
```

**Response** (202 Accepted):

```json
{
  "id": 33,
  "jobStatus": "Process",
  "forecast": "demo-jan2025-unique-id",
  "periodName": "2025-01",
  "jobStart": "2025-03-09T11:59:13Z",
  "jobEnd": null
}
```

| Field | Description |
|-------|-------------|
| `forecast` | Unique string to isolate this simulation. Required to avoid blocking other jobs. |
| `employeeIdentifiers` | Array of employee identifiers (email/ID). Limits calculation to listed employees. |
| `periodStart` | First day of the pay period (ISO 8601 with timezone). |

### Poll until completion

```bash title="GET /payruns/jobs/{payrunJobId}"
curl -s "$GW/partners/$PARTNER_ID/tenants/$TENANT_ID/payruns/jobs/33" \
  -H "Authorization: Bearer $KEY"
```

Wait until `jobStatus` changes from `Process` to `Draft` and `jobEnd` is no longer `null`. Typical processing time: **1-12 seconds**.

### Retrieve results

Forecast results are available immediately once the job reaches `Draft` — no additional steps needed.

```bash title="GET /payrollresults/sets?payrunJobId={jobId}"
curl -s "$GW/partners/$PARTNER_ID/tenants/$TENANT_ID/payrollresults/sets?payrunJobId=33" \
  -H "Authorization: Bearer $KEY"
```

That's it — **3 API calls** to run a complete payroll: start job, poll status, get results.

For more details on forecast vs real mode, see the [Real vs Forecast Mode](/guides/real-vs-forecast) guide.

## Step 5 — Read the results

The response from `/payrollresults/sets` contains `wageTypeResults` (individual payslip lines) and `collectorResults` (aggregated totals).

### Example output

Input: 151.67h at 13.00 EUR/h, 20 meals at 20.70 EUR, prevoyance 0.89%, PAS 5%.

#### Wage Types

| WT | Name | Value (EUR) |
|----|------|------------|
| 100 | SalaireBase | 1,971.71 |
| 200 | AssuranceMaladiePatronale | 138.02 |
| 210 | VieillessePlafonneeSalariale | 136.05 |
| 211 | VieillessePlafonneePatronale | 168.58 |
| 212 | VieillesseDeplafonneeSalariale | 7.89 |
| 213 | VieillesseDeplafonneePatronale | 39.83 |
| 220 | AllocationsFamiliales | 68.02 |
| 230 | AccidentsTravail | 76.31 |
| 240 | FNAL | 9.86 |
| 241 | CSA | 5.92 |
| 253 | PrevoyancePatronale | 17.55 |
| 300 | CSGDeductible | 132.92 |
| 301 | CSGNonDeductible | 46.91 |
| 302 | CRDS | 9.77 |
| 400 | AgircArrcoT1Salariale | 62.11 |
| 401 | AgircArrcoT1Patronale | 93.06 |
| 410 | CEGT1Salariale | 16.96 |
| 411 | CEGT1Patronale | 25.44 |
| 450 | AssuranceChomage | 78.87 |
| 451 | AGS | 4.93 |
| 499 | ReductionFillonTotale | -490.96 |
| 500 | ReductionFillonURSSAF | -399.69 |
| 501 | ReductionFillonAGIRC | -91.27 |
| 600 | IndemniteRepas | 414.00 |

#### Totals

| WT | Name | Value (EUR) |
|----|------|------------|
| 900 | TotalBrut | 1,971.71 |
| 910 | TotalCotisationsSalariales | 412.61 |
| 920 | TotalCotisationsPatronales | 747.40 |
| 930 | TotalAllegements | -490.96 |
| 950 | NetImposable | 1,615.79 |
| 960 | PAS | 80.79 |
| 980 | TotalIndemnites | 414.00 |
| **990** | **NetAPayer** | **1,892.31** |

## Troubleshooting

### "Payrun with id X has already a payrun job with status Draft"

Existing Draft jobs block new non-forecast job creation. Solutions:

1. **Use forecast mode** — forecast jobs don't block each other
2. Delete the blocking job via the backend API
3. Advance the blocking job through its lifecycle (Release → Process → Complete)

### No results returned

If `payrollresults/sets?payrunJobId={id}` returns an empty array:

1. Verify the job completed successfully (`jobEnd` is not null, `processedEmployeeCount` > 0)
2. Check that `ParametresLegaux` case values exist (PSS, SMIC) — without them, capped contributions return 0
3. Ensure all required case values were injected for the employee

### Missing case values

Each employee needs at minimum:

| Case | Required Fields |
|------|----------------|
| `Contrat` | DateDebutContrat |
| `Remuneration` | HeuresTravaillees, TauxHoraire |
| `Frais` | NombreRepas, **MontantRepas** (commonly forgotten) |
| `PrevoyanceMutuelle` | HasPrevoyance, TauxPrevoyance, HasMutuelleObligatoire, HasMutuelleFacultative |
| `PAS` | TauxPAS |
