The Skills Fest

Sync guide

The first run is the one that matters. Get it wrong and every member exists twice.

The problem

A workspace already has people in it, typed in by hand. Your system has the same people. Nothing links the two, so a naive first push creates a second copy of everyone — and because families share email addresses, an address cannot be used to tell them apart.

So we keep an identity map: your external_id ↔ our member. Once a person is in it, every later run updates rather than adds.

Step 1 — preview

Send the batch with "mode": "dry_run". Nothing is written. For any record that looks like someone already here, you get candidates back.

curl -X POST https://api.theskillsfest.com/v1/sync/members \
  -H "Authorization: Bearer $SKILLSFEST_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "source": "acme-membership",
    "mode": "dry_run",
    "records": [
      { "external_id": "U-1042", "first_name": "Ada", "last_name": "Lovelace",
        "date_of_birth": "2012-04-05", "role": "Student" }
    ]
  }'

Step 2 — match

Confirm the matches. An admin can do this in Settings → API → Sync, or you can post them:

curl -X POST https://api.theskillsfest.com/v1/sync/links \
  -H "Authorization: Bearer $SKILLSFEST_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "source": "acme-membership",
        "links": [{ "external_id": "U-1042", "member_id": 1489 }] }'

You can ask us to link obvious matches for you with "auto_link": "exact", which links only when the last name, date of birth, and club all agree and exactly one person matches. If several people match, we refuse and hand it back — that means the workspace has its own duplicates, and linking into an unresolved pair strands half of someone’s history.

Step 3 — apply, and keep applying

curl -X POST https://api.theskillsfest.com/v1/sync/members \
  -H "Authorization: Bearer $SKILLSFEST_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "source": "acme-membership", "records": [ ... ] }'

Send the same batch as often as you like. Each record comes back as created, updated, unchanged, or failed, and an unchanged record does no work at all. Partial success is normal: one bad row is a failed entry, not a rejected batch.

The field contract

Send only fields Skills Fest actually stores. Anything else is refused, naming the field — we never accept a write and quietly drop part of it, because an integration that believes it recorded something and didn’t is worse than one that got an error.

If your system calls itSend
a stable record id, e.g. ymms:usuario:1463299external_id verbatim, with your source slug alongside it
one display name, “FIRST LAST”first_name and last_name — both required and non-empty, so split before sending
the person’s club idunit_external_id
the club’s director or head leaderon the CLUB record, director_external_id — the same id you imported that person under, from the same source. There is no director_email and no name matching: two clubs in different programmes share names and leaders far too often for either to be an identity
a role like PATHFINDER or DIRECTORStudent, Teacher, Staff, or UnitLeader. Map it on your side — an unmapped role fails the record rather than guessing whether someone is a child or an adult
a regional grouping or conferenceparent_org on the club
active: falsestatus: "inactive"
screening / background-check statebackground_check and background_check_not_cleared as booleans you have already decided. We never infer them from a role — see below

Fields with no Skills Fest equivalent — a church, an age, a city, a member count, a status string — are refused. Send what maps; drop the rest.

Moving someone to another club

Nothing special: send the same external_id with the new unit_external_id. A person belongs to one club here, so the old one stops listing them automatically. Sync the destination club first if it is new.

When someone leaves

By default, leaving a person out of a batch does nothing — most callers send changes rather than a census, and a truncated payload must never deactivate anyone. Two ways to retire someone:

  • Send them with "status": "inactive". Simple, explicit, always works.
  • Or send "roster": "full", which declares the payload is your complete roster for this source. Anyone linked to it and absent is deactivated — never deleted, and never anyone who isn’t linked to your source.

A full-roster run that would deactivate an unusual share of your people stops and tells you the number instead of doing it. Resend with "confirm_deactivations": <that exact number> if it really is intended. It’s an exact count rather than a flag on purpose: a bug that truncates your payload can always set a flag, but it cannot guess how many people it is about to remove.

Rules worth knowing before you start

  • Nothing here ever deletes a person. Deactivating keeps their record and their history; it only takes them off the active roster.
  • Clubs first — then clubs again, if you set directors. Sync clubs before people so a member’s unit_external_id resolves. An unknown club fails that record rather than filing the person under nothing. And a club whose NAME matches one you already have is refused, not merged — two unrelated clubs can share a name, so a person has to confirm that link in Settings → API → Sync.
    A club’s director_external_id points the other way, at a person, so on a first run that person does not exist yet. Run clubs → members → clubs: the second club pass costs nothing for clubs that have not changed (they come back unchanged) and fills in the directors. A director who cannot be resolved fails that club outright — it will not quietly land without a leader.
  • Some fields are ours. Dietary needs, check-in numbers, registrations, and attendance are maintained here and cannot be overwritten — a stale allergy note is a safety problem, not a freshness one.
  • Role and username are set once, at creation, and never overwritten.
  • Background-check changes are audited and escalated. Every change records who made it; a first-ever or bulk change emails the workspace Owner.
  • Batches are capped at 500 records.