Intranet Sync API Reference
Authentication
All endpoints require Bearer token authentication:
Authorization: Bearer <token>
Tokens are managed at /admin/tokens. Stored as bcrypt hashes; plaintext is shown only once at creation.
Token model
type APIToken struct {
gorm.Model
Name string // Friendly name
TokenHash string // bcrypt hash
IPAllowlist string // "192.168.1.0/24, 10.0.0.0/8"
LastUsedAt *time.Time
LastUsedIP string
Active bool
CreatedByID uint
}
IP allowlist format
- Single IPv4:
192.168.1.1 - IPv4 CIDR:
192.168.1.0/24 - Single IPv6:
2001:db8::1 - IPv6 CIDR:
2001:db8::/32 - Mixed:
192.168.1.1, 10.0.0.0/8, 2001:db8::1 - Empty: all IPs allowed
Authentication errors
| HTTP Status | Code | Cause |
|---|---|---|
| 401 | MISSING_AUTH_HEADER |
No Authorization header |
| 401 | INVALID_AUTH_FORMAT |
Not Bearer scheme |
| 401 | EMPTY_TOKEN |
Empty token value |
| 401 | INVALID_TOKEN |
Token not found or inactive |
| 403 | IP_NOT_ALLOWED |
Client IP not in allowlist |
API Endpoints
Base URL: /api/intranet
POST /api/intranet/person
Syncs person records (parents, children, employees).
Request:
{
"records": [
{
"external_id": "P12345",
"vorname": "Max",
"nachname": "Mustermann",
"geburtstag": "2018.03.15",
"rolle": "1",
"comments": "Optional notes"
}
]
}
| Field | Type | Required | Description |
|---|---|---|---|
| external_id | string | Yes | Unique identifier from intranet |
| vorname | string | No | First name |
| nachname | string | No | Last name |
| geburtstag | string | No | Birthday (YYYY.mm.dd) |
| rolle | string | No | 1=child, 2=father, 3=mother, 4=other |
| comments | string | No | Additional notes |
Upsert key: external_id
POST /api/intranet/parent-child
Syncs parent-child relationships.
Request:
{
"records": [
{
"eltern_id": "P001",
"kind_id": "K001",
"von": "2023.01.01",
"bis": null
}
]
}
| Field | Type | Required | Description |
|---|---|---|---|
| eltern_id | string | Yes | Parent's external ID |
| kind_id | string | Yes | Child's external ID |
| von | string | No | Validity start (YYYY.mm.dd) |
| bis | string | No | Validity end (YYYY.mm.dd) |
Upsert key: eltern_id + kind_id (composite)
POST /api/intranet/belegung/krp
Syncs group assignments from KRP source table.
Request:
{
"records": [
{
"id_krp": "KRP001",
"kind_id": "K001",
"gruppen_id": "G001",
"tage_binaer": 31,
"anzahl_tage": 5,
"von": "2023.08.01",
"bis": "2024.07.31",
"status": 3,
"comments": "",
"some_id": null
}
]
}
Upsert key: id_krp
POST /api/intranet/belegung/ue3
Same structure as /belegung/krp but uses id_ue3 as the upsert key.
Status values: 2 = Planning, 3 = In contract (active)
POST /api/intranet/employee
Syncs employee records.
Request:
{
"records": [
{
"external_id": "E001",
"name": "Maria Schmidt",
"vorname": "Maria",
"nachname": "Schmidt",
"qualifikation": "Erzieherin",
"von": "2020.01.01",
"bis": null
}
]
}
Upsert key: external_id
POST /api/intranet/location
Syncs facility/location records.
Request:
{
"records": [
{
"einrichtungs_id": "L001",
"name": "Kita Sonnenschein",
"adresse": "Hauptstraße 1, 12345 Musterstadt",
"reihenfolge": 1,
"g_von": "2020.01.01",
"g_bis": null
}
]
}
Upsert key: einrichtungs_id
POST /api/intranet/group
Syncs group records.
Request:
{
"records": [
{
"gruppen_id": "G001",
"einrichtungs_id": "L001",
"e_art_id": "ART1",
"reihenfolge": 1,
"name": "Schmetterlinge",
"oez": "07:00-17:00",
"g_von": "2020.01.01",
"g_bis": null
}
]
}
Upsert key: gruppen_id
POST /api/intranet/location-lead
Syncs location leadership assignments.
Request:
{
"records": [
{
"einrichtungs_id": "L001",
"mitarbeiter_id": "E001",
"stellvertreter_id": "E002",
"stellvertreter_anteil": 50,
"g_von": "2023.01.01",
"g_bis": null
}
]
}
Upsert key: einrichtungs_id + mitarbeiter_id (composite)
POST /api/intranet/group-lead
Syncs group leadership assignments.
Request:
{
"records": [
{
"gruppen_id": "G001",
"mitarbeiter_id": "E003",
"g_von": "2023.01.01",
"g_bis": null
}
]
}
Upsert key: gruppen_id + mitarbeiter_id (composite)
Response format
Success
{
"success": true,
"received": 3,
"inserted": 2,
"updated": 1,
"errors": 0,
"results": [
{"identifier": "P001", "status": "inserted"},
{"identifier": "P002", "status": "updated"},
{"identifier": "P003", "status": "inserted"}
]
}
Partial failure
{
"success": false,
"received": 3,
"inserted": 1,
"updated": 1,
"errors": 1,
"results": [
{"identifier": "P001", "status": "inserted"},
{"identifier": "P002", "status": "updated"},
{"identifier": "P003", "status": "error", "error": "duplicate key violation"}
]
}
Staging tables
All staging tables use the sync_ prefix.
sync_persons
| Column | Type | Description |
|---|---|---|
| id | uint | Internal PK |
| external_id | varchar(50) | Intranet person ID (unique) |
| vorname | varchar(100) | First name |
| nachname | varchar(100) | Last name |
| geburtstag | date | Birthday |
| rolle | varchar(20) | 1=child, 2=father, 3=mother, 4=other |
| comments | text | Additional notes |
| synced_at | timestamp | Last sync time |
sync_parent_children
| Column | Type | Description |
|---|---|---|
| id | uint | Internal PK |
| eltern_id | varchar(50) | Parent external ID |
| kind_id | varchar(50) | Child external ID |
| von | date | Validity start |
| bis | date | Validity end |
| synced_at | timestamp | Last sync time |
Composite key: eltern_id + kind_id
sync_belegungen
| Column | Type | Description |
|---|---|---|
| id_internal | uint | Internal PK |
| id_krp | varchar(50) | KRP source ID (nullable) |
| id_ue3 | varchar(50) | UE3 source ID (nullable) |
| kind_id | varchar(50) | Child external ID |
| gruppen_id | varchar(50) | Group external ID |
| tage_binaer | int | Binary care day flags |
| anzahl_tage | int | Number of care days |
| von | date | Assignment start |
| bis | date | Assignment end |
| status | int | 2=planning, 3=in contract |
| comments | text | Notes |
| some_id | varchar(50) | Additional reference ID |
| synced_at | timestamp | Last sync time |
sync_employees
| Column | Type | Description |
|---|---|---|
| id | uint | Internal PK |
| external_id | varchar(50) | Employee ID (unique) |
| name | varchar(200) | Full name |
| vorname | varchar(100) | First name |
| nachname | varchar(100) | Last name |
| qualifikation | varchar(100) | Qualification/role |
| von | date | Employment start |
| bis | date | Employment end |
| synced_at | timestamp | Last sync time |
sync_locations
| Column | Type | Description |
|---|---|---|
| id | uint | Internal PK |
| einrichtungs_id | varchar(50) | Location ID (unique) |
| name | varchar(200) | Location name |
| adresse | text | Address |
| reihenfolge | int | Sort order |
| g_von | date | Valid from |
| g_bis | date | Valid until |
| synced_at | timestamp | Last sync time |
sync_groups
| Column | Type | Description |
|---|---|---|
| id | uint | Internal PK |
| gruppen_id | varchar(50) | Group ID (unique) |
| einrichtungs_id | varchar(50) | Parent location ID |
| e_art_id | varchar(50) | Type ID |
| reihenfolge | int | Sort order |
| name | varchar(200) | Group name |
| oez | varchar(100) | Opening hours |
| g_von | date | Valid from |
| g_bis | date | Valid until |
| synced_at | timestamp | Last sync time |
sync_location_leads
| Column | Type | Description |
|---|---|---|
| id | uint | Internal PK |
| einrichtungs_id | varchar(50) | Location ID |
| mitarbeiter_id | varchar(50) | Employee ID (lead) |
| stellvertreter_id | varchar(50) | Deputy employee ID |
| stellvertreter_anteil | int | Deputy percentage |
| g_von | date | Valid from |
| g_bis | date | Valid until |
| synced_at | timestamp | Last sync time |
Composite key: einrichtungs_id + mitarbeiter_id
sync_group_leads
| Column | Type | Description |
|---|---|---|
| id | uint | Internal PK |
| gruppen_id | varchar(50) | Group ID |
| mitarbeiter_id | varchar(50) | Employee ID (lead) |
| g_von | date | Valid from |
| g_bis | date | Valid until |
| synced_at | timestamp | Last sync time |
Composite key: gruppen_id + mitarbeiter_id
Admin interface
Sync dashboard routes
| Method | Path | Description |
|---|---|---|
| GET | /admin/sync | Show dashboard with pending counts |
| POST | /admin/sync/all | Process all data types |
| POST | /admin/sync/locations | Process locations only |
| POST | /admin/sync/groups | Process groups only |
| POST | /admin/sync/children | Process children only |
| POST | /admin/sync/childgroups | Process child-group assignments |
Outbound API: Employee Daily Group Sync
The Wippidu app makes outbound calls to the intranet system to fetch daily group assignments for employees. This determines which group(s) an employee is assigned to for the current day.
Configuration
Settings are managed at /admin/settings under the Intranet tab:
| Setting | Description |
|---|---|
| Enabled | Enable/disable intranet sync |
| API URL | Full URL to the intranet groups endpoint |
| API Token | Authentication token for the intranet API |
Request format
Method: POST Content-Type: application/x-www-form-urlencoded
The app sends a form-encoded POST request with both form fields and an Authorization header:
POST <API_URL>
Content-Type: application/x-www-form-urlencoded
Authorization: Bearer <API_TOKEN>
Token=<API_TOKEN>&Mitarbeiter_ID=<EMPLOYEE_EXTERNAL_ID>
| Field | Description |
|---|---|
| Token | API token (capital T) |
| Mitarbeiter_ID | Employee's ExternalID from user record |
Note: The token is sent both as a form field (Token) and as a Bearer token in the Authorization header. This dual-authentication was required by the intranet API.
Response format
The intranet API returns JSON with an array of group assignments:
{
"records": [
{
"Gruppen_ID": "G001",
"Stammteam": false
},
{
"Gruppen_ID": "G002",
"Stammteam": true
}
]
}
| Field | Type | Description |
|---|---|---|
| Gruppen_ID | string | Group's external ID (must match groups.external_id in app) |
| Stammteam | boolean | true = core team member, false = substitute |
Important: Stammteam is a JSON boolean (true/false), not an integer.
Group assignment prioritization
When the API returns multiple records, the app uses this priority logic:
- Substitute groups first: If any records have
Stammteam=falseAND the group exists in the app, use those - Fallback to core team: If no substitute groups exist in the app, use
Stammteam=truegroups - Skip unknown groups: Groups not found in the app (by
external_id) are logged and skipped
This ensures employees see children from the group where they're actually working today, not their home group.
Access vs. creation rights
The daily group assignments affect two different permission levels:
| Access Type | Function | Description |
|---|---|---|
| Viewing | GetEmployeeGroups() |
Returns all daily groups (substitute or core team) |
| Creating | GetEmployeeCoreTeamGroups() |
Returns only core team groups (Stammteam=true) |
Effect: A substitute employee can VIEW children and messages in their substitute group, but cannot CREATE news, parental letters, or calendar entries there. Creation rights remain with their home (core team) group.
Database storage
Daily assignments are stored in the employee_daily_groups table:
| Column | Type | Description |
|---|---|---|
| id | uint | Primary key |
| user_id | uint | FK to users.id |
| group_id | uint | FK to groups.id |
| assignment_date | date | The date of the assignment |
| is_core_team | bool | True if this is a core team assignment |
| created_at | timestamp | Record creation time |
Unique constraint: (user_id, group_id, assignment_date)
Refresh trigger
The refresh is triggered automatically when an employee with an ExternalID logs in and:
- intranet_refresh_date is NULL, OR
- intranet_refresh_date is not today
The refresh status is tracked on the user record:
| Column | Type | Description |
|---|---|---|
| intranet_refresh_date | date | Last successful refresh date |
| intranet_refresh_failed | bool | True if last refresh attempt failed |
Security
- Token storage: bcrypt hashes only — plaintext never persisted
- Token display: plaintext shown only once at creation
- IP allowlist: optional but recommended for production
- Token rotation: regenerate capability without service interruption
- HTTPS: required for all production traffic
- Payload validation: all inputs validated via struct tags before processing
- SQL injection prevention: GORM parameterized queries throughout