Switch Connect
A read-only API that returns the shift-level labor Switch staffed in your buildings — one facility or your whole portfolio, for the dates you ask for, on whatever schedule your team runs.
Switch Connect is a read-only API. Your team makes the call, Switch returns the labor records for the facilities and dates you asked for. Nothing is scheduled and nothing lands in an inbox — you pull what you need, when you need it.
Before this, moving Switch labor data into your systems meant a file on a schedule and someone to reconcile it. Switch Connect removes the file. The same data goes directly into payroll, PPD and PBJ reporting, your BI stack, or the dashboard your team already runs.
Two things shape every request:
Switch issues four things. If any of them are missing, open a support ticket and we’ll get them to you before you write a line of code.
| Item | What it is |
|---|---|
| slug | A short identifier for your organization that appears in every URL — for example, example-health. |
| username + secret | Your API credentials, issued by Switch and delivered by a one-time secure link that expires once it’s opened. Move the secret into your secrets manager as soon as it arrives — Switch can’t show it to you a second time and will reissue instead. Never sent in an email body, never posted on a page. |
| facility IDs | Switch’s internal ID for each of your buildings, delivered as a mapping sheet alongside your own facility names and codes. Keep it — every per-facility call uses these numbers. |
| response format | JSON or CSV. This is set per organization rather than per request, so every call against your slug returns the same format. Switch confirms which one you’re on. |
Everything on this page is identical for every Switch Connect client. The parts specific to you — your slug, your response format, and your facility mapping — sit on a private setup page that Switch issues you, behind its own password.
Your credentials aren’t on that page either. They arrive by one-time link, so the secret never rests in a web page or an email body.
Everything runs off one base URL:
https://api-v2.switchcare.com/v1/integrations/{slug}/labor-export| Placeholder | Replace with |
|---|---|
| {slug} | Your assigned integration slug — for example, example-health. |
| {facility_id} | Switch’s internal facility ID, from the mapping sheet — for example, 1042. |
| YYYY-MM-DD | A calendar date in the facility’s local time — for example, 2026-05-02. |
Switch Connect uses HTTP Basic authentication over TLS. Send the username and secret Switch issued you on every request — there is no token to fetch and no session to refresh.
curl -u "{username}:{secret}" \ "https://api-v2.switchcare.com/v1/integrations/{slug}/labor-export?date=YYYY-MM-DD"
Most HTTP clients take the pair directly; anything that doesn’t will accept the equivalent header, Authorization: Basic <base64 of username:secret>. Send it over HTTPS only — Basic credentials are encoded, not encrypted.
You’re making two choices on every request: how many facilities and how many days. That gives you four URLs. Placeholders are marked in coral.
Add the /facility/{facility_id} segment. Use this when a single building’s data feeds a building-level system, or when you want to isolate one facility while testing.
https://api-v2.switchcare.com/v1/integrations/{slug}/labor-export/facility/{facility_id}?date=YYYY-MM-DD
https://api-v2.switchcare.com/v1/integrations/{slug}/labor-export/facility/{facility_id}?start_date=YYYY-MM-DD&end_date=YYYY-MM-DD
Drop the /facility/… segment entirely. One call returns every facility mapped to your slug — this is the one most teams run on a schedule.
https://api-v2.switchcare.com/v1/integrations/{slug}/labor-export?date=YYYY-MM-DD
https://api-v2.switchcare.com/v1/integrations/{slug}/labor-export?start_date=YYYY-MM-DD&end_date=YYYY-MM-DD
| Parameter | Where | Format | Notes |
|---|---|---|---|
| slug | Path | string | Required on every call. Assigned by Switch. |
| facility_id | Path | integer | Optional. Include the /facility/{facility_id} segment for one facility; omit it for all mapped facilities. |
| date | Query | YYYY-MM-DD | A single calendar day, matched against each shift’s clock-in date. Do not send alongside start_date or end_date. |
| start_date | Query | YYYY-MM-DD | First day of the window, inclusive. Matched against clock-in date. Must be paired with end_date. |
| end_date | Query | YYYY-MM-DD | Last day of the window, inclusive — a shift starting on this date is returned with its full hours, overnight carry-over included. Must be paired with start_date. |
Five behaviors account for nearly every surprise teams hit in the first week.
date, or start_date and end_date together. Sending both is rejected.400. For longer history, split the request into consecutive windows — see How teams use it.date call covers each building’s own local day.shifts: [], or a CSV with only its header row. It is never a 404. Build your loader to treat empty as “nothing happened that day,” not as a failure.One facility, a range that clears the cap:
https://api-v2.switchcare.com/v1/integrations/example-health/labor-export/facility/1042?start_date=2026-05-02&end_date=2026-07-03
| example-health | The organization’s slug |
| 1042 | One facility, by Switch’s facility ID |
| 2026-05-02 → 2026-07-03 | Every shift clocking in from May 2 through July 3, both days included — comfortably inside the 90-day cap |
Drop /facility/1042 from that URL and the same date window returns every facility mapped to example-health.
A response is an envelope describing what you asked for, wrapping a shifts array. Facility and date sit on the envelope; each object in the array is one worked shift.
| Field | What it holds |
|---|---|
| facility_id | Switch’s facility ID for the building this response covers. |
| facility_name | That building’s name. |
| date | The date the response covers. |
| generated_at | UTC timestamp of when Switch built the response. Worth storing alongside the load — it’s what tells you which pull a given row came from. |
| shifts | Array of shift objects. Empty when nothing was worked. |
| Field | Type | What it holds |
|---|---|---|
| shift_id | integer | Identifies the shift. Stable across re-pulls, but not unique within a response — a shift paid under more than one code returns a row per segment, all carrying this ID. See One shift, more than one row. |
| employee_id | integer | The provider’s permanent Switch ID. It doesn’t change between quarters or across buildings, so one person carries one ID everywhere they work for you. |
| employee_first_name | string | Provider’s first name. |
| employee_last_name | string | Provider’s last name. |
| job_code | string · nullable | Short role code, e.g. CNA. Can be null — see below. |
| job_desc | string · nullable | Full role name, e.g. Certified Nursing Assistant. Null whenever job_code is. |
| work_date | YYYY-MM-DD | The shift’s clock-in date in the facility’s local time. This is what your date filter matches and the day the whole shift is attributed to. |
| clock_in | ISO 8601 | Full timestamp with UTC offset, e.g. 2026-08-17T14:00:00-05:00. The offset is the facility’s, so local time is readable without a timezone lookup. |
| clock_out | ISO 8601 | Same format. On an overnight shift this carries the following calendar date, while work_date stays on the start date. |
| break_minutes | integer | Unpaid break, in whole minutes. |
| total_hours_worked | decimal | Paid hours, net of the break — see below. |
| pay_code | string | Which code the row’s hours were billed under — regular, weekend, overtime, holiday. Together with shift_id it identifies the row. |
| bill_rate | number | Hourly rate billed for the shift. |
One facility, one date, two of its shifts:
{
"facility_id": "1042",
"facility_name": "Example Care Center",
"date": "2026-08-17",
"generated_at": "2026-08-18T02:01:36.864Z",
"shifts": [
{
"shift_id": 10001,
"employee_id": 4021,
"employee_first_name": "Dana",
"employee_last_name": "Whitfield",
"job_code": "CNA",
"job_desc": "Certified Nursing Assistant",
"work_date": "2026-08-17",
"clock_in": "2026-08-17T06:00:00-05:00",
"clock_out": "2026-08-17T13:30:00-05:00",
"break_minutes": 30,
"total_hours_worked": 7,
"pay_code": "R",
"bill_rate": 24
},
{
"shift_id": 10002,
"employee_id": 4088,
"employee_first_name": "Marcus",
"employee_last_name": "Ellery",
"job_code": null, // unclassified — see below
"job_desc": null,
"work_date": "2026-08-17",
"clock_in": "2026-08-17T14:00:00-05:00",
"clock_out": "2026-08-17T22:00:00-05:00",
"break_minutes": 14,
"total_hours_worked": 7.77,
"pay_code": "R",
"bill_rate": 24
}
]
}When nothing was worked, the envelope still comes back with an empty array:
{ "facility_id": "1042", "facility_name": "Example Care Center", "date": "2026-08-17",
"generated_at": "2026-08-18T02:01:36.864Z", "shifts": [] }A response is one row per shift and pay code — not one row per shift. When a single shift is billed under more than one code, each segment comes back as its own object, and they all carry the same shift_id.
An overnight that crosses into a weekend is the usual cause, but anything that changes the code mid-shift does it — hours tipping into overtime, a shift running into a holiday.
{
…
"shifts": [
{
"shift_id": 10003, // same shift
"work_date": "2026-08-17",
"clock_in": "2026-08-17T19:00:00-05:00",
"clock_out": "2026-08-18T07:30:00-05:00",
"break_minutes": 30,
"total_hours_worked": 5, // hours billed under this code
"pay_code": "R"
},
{
"shift_id": 10003, // same shift, second segment
"work_date": "2026-08-17",
"clock_in": "2026-08-17T19:00:00-05:00",
"clock_out": "2026-08-18T07:30:00-05:00",
"break_minutes": 30,
"total_hours_worked": 7,
"pay_code": "RW"
}
]
}This is not duplication, and it is not a conflict. The two rows describe different parts of one shift.
Everything except total_hours_worked and pay_code repeats on every segment, because those fields describe the whole shift rather than the slice. The clock times are the shift’s, and so is break_minutes — it is the one break, reported on each row, not a break per segment.
shift_id alone. The second segment overwrites the first, its hours vanish from your totals, and nothing errors. Key on shift_id and pay_code together.break_minutes across rows. A two-segment shift then shows a 60-minute break where 30 were taken. Take the break once per shift_id, not once per row.total_hours_worked is the clocked span minus the break, rounded to two decimals:
(clock_out − clock_in) − break_minutes = total_hours_worked
06:00 → 13:30 = 450 min − 30 = 420 min = 7.00
14:00 → 22:00 = 480 min − 14 = 466 min = 7.77When a shift is split across pay codes, that same figure is the sum of its segments:
19:00 → 07:30 = 750 min − 30 = 720 min = 12.00
regular 5.00
weekend 7.00
-----
12.00 ← matches the net clocked spanSo a shift’s true hours are the total across every row sharing its shift_id, and that total reconciles against the clock. It is the check worth building into your load.
Do not subtract break_minutes again. It is reported so you can show or audit it, not so you can apply it — the break is already out of the total. Double-subtracting understates every shift you report, and it fails silently.
job_code and job_desc can be null. A shift with no role attached still carries real hours. Decide up front whether those hours are excluded, bucketed as unclassified, or held for review — don’t let them fall through a lookup and vanish.employee_id can appear several times on one date — separate shifts, or separate pay-code segments of one shift.Because work_date is the clock-in date, consecutive windows never double-count and never drop an overnight shift. A window ending March 31 and the next one starting April 1 partition the data cleanly, even where a shift ran through midnight on the 31st — those hours belong to March in full.
If your slug is set to CSV you get the same shift fields flattened to a header row plus one row per shift, with the envelope’s facility and date repeated on each row.
/facility/… segment, and how each shift is attributed to its building.date holds on a range request, given the envelope carries a single date on a single-date call.pay_code set, beyond R.job_code values map straight to the codes your downstream system expects, or need a lookup on your side.| Code | Meaning | What causes it |
|---|---|---|
| 200 | Success | The request was valid. The response may legitimately contain zero records — see Rules that matter. |
| 400 | Bad request | A date window wider than 90 days; date sent together with start_date/end_date; or a date that isn’t formatted YYYY-MM-DD. |
| — | Authentication failure | Missing or incorrect credentials. Exact code to be confirmed by Switch. |
| — | Unknown slug, or a facility not mapped to you | A facility ID outside your mapped list, or a slug that doesn’t exist. Exact code to be confirmed by Switch. |
If a call fails and the cause isn’t obvious from the list above, open a support ticket with the full URL and the timestamp — secret removed — and we can trace it.
One call, all facilities, yesterday’s date. It’s a single request for the whole portfolio and it keeps your load idempotent — re-running the same day overwrites cleanly rather than appending.
https://api-v2.switchcare.com/v1/integrations/{slug}/labor-export?date=YYYY-MM-DD # yesterday, per facility-local date
Because a window caps at 90 days, a year of history is a loop of consecutive windows rather than one large request. Chunk at 60 days rather than 90 — it leaves room to shift a boundary without recalculating the whole sequence.
# a year, in 60-day windows ?start_date=2025-09-01&end_date=2025-10-30 ?start_date=2025-10-31&end_date=2025-12-29 ?start_date=2025-12-30&end_date=2026-02-27 …and so on
When one facility’s numbers look wrong, pull that facility on its own for the period in question. It’s a much smaller payload to eyeball against your own records than a portfolio-wide response.
Timecards are sometimes corrected after a shift closes. Ask Switch how a corrected record behaves when you re-pull a date you’ve already loaded, and set your reconciliation window accordingly.
Anything the pages above don’t answer — a call returning an error you can’t place, a facility missing from your mapping, a credential that needs reissuing — goes through a support ticket. It’s tracked, and it doesn’t depend on one person being at their desk.
Tell us what you were calling and what came back. We'll pick it up from there.
You'll get a confirmation by email and someone from Switch will follow up.
For a failing call, the full request URL and the timestamp are usually enough to trace it — with the secret removed. Never paste credentials into a ticket, an email, or a screenshot. If you already have, say so in the ticket and Switch will rotate them.