Skip to content

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:

  1. Substitute groups first: If any records have Stammteam=false AND the group exists in the app, use those
  2. Fallback to core team: If no substitute groups exist in the app, use Stammteam=true groups
  3. 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

  1. Token storage: bcrypt hashes only — plaintext never persisted
  2. Token display: plaintext shown only once at creation
  3. IP allowlist: optional but recommended for production
  4. Token rotation: regenerate capability without service interruption
  5. HTTPS: required for all production traffic
  6. Payload validation: all inputs validated via struct tags before processing
  7. SQL injection prevention: GORM parameterized queries throughout