56 "decision": "One record per contact: { contactId, assignTags: { privateTags: { tagIds } } }, contactId in the body as well as the path so the readiness grant checks both references. Assign-only (never unassign), so a retry or unknown outcome resends safely. Read back GET /contacts/v5/contacts/{id}?fields=TAGS and check every tag id is present. The crosswalk row maps the source assignment key to the contact id."
57 },
58 "reliability": {
59 "status": "reliable",
60 "flags": []
61 },
62 "pitfalls": [
63 {
64 "code": "unknown-tag-id-accepted",
65 "severity": "blocker",
66 "summary": "VERIFIED LIVE 2026-10-27 : update-tags stores a tag id that does not exist (e.g. 'zzzzz'). Tag ids come only from read-back crm/label records via the readiness grant."
67 },
68 {
69 "code": "path-wins-over-body",
70 "severity": "warning",
71 "summary": "VERIFIED LIVE 2026-10-27: when body contactId differs from the path, Wix writes the path contact. The builder builds both from the same contactId."
72 },
73 {
74 "code": "not-in-contact-upsert",
75 "severity": "warning",
76 "summary": "VERIFIED LIVE 2026-10-27: tags sent inside the default (OVERWRITE) contact upsert replace a matched contact's existing tags; OVERWRITE without tags keeps them. Labels go on contacts through this entity only."
77 },
78 {
79 "code": "member-contact",
80 "severity": "info",
81 "summary": "A member's contact is a crm/contact produced as the member write's secondary output; the per-record crm/contact reference resolves it the same way."
82 }
83 ],
84 "mappingGuidance": [
85 "Dependencies: crm/contact (contactId) and crm/label (each tag id) are per-record; a contact with no source labels has no record here and never waits.",
86 "Emit one record per contact that has labels: contactId = resolveReference('crm/contact', contactKey, 'contactId'), each tag id = resolveReference('crm/label', labelKey, 'assignTags.privateTags.tagIds[i]'). Send with assignContactLabels(recordClient, { contactId, tagIds })."
98 "note": "Update Contact Tags: assign/unassign private tags on one contact"
99 },
100 {
101 "type": "live-run",
102 "path": "anonymized-live-verification",
103 "note": "Verified live 2026-10-27 (probe P1/P3/P4, throwaway site): update-tags with label.tagId -> tag on contact, repeat harmless; bulk upsert OVERWRITE replaces tags; body/path contactId mismatch writes the path contact; missing contact 404; unknown tag id accepted (trap); probe records deleted"
104 }
105 ],
106 "notes": "Added 2026-10-27 so source labels land on contacts. Kept apart from crm/contact so contacts without labels never wait for labels and the contact writers stay unchanged."