Skip to main content

API Contract: Mention User in CDP Notes

Derived from rfc-notes-mention-user.md Detail 2.4 / 2.A / 1.C / 1.A, cross-referenced against rfc-notes-mention-user.task-breakdown.md for task ids, and grounded against the real BE code at ../../../contact-service (relative to this repo's checkout — see root CLAUDE.md sibling-checkout convention) and the real FE code at ../../../qontak-customer-fe. This file is a projection, not a new source of truth — if a contract here needs to change, change the RFC first (a new Decision entry if it's material), then regenerate this file. Composite AC ids follow the PRD's <STORY-ID>/AC-n convention.

Grounding legend: 🟢 verified against real code (file:line cited) · 🟡 proposed by this RFC, not yet in the codebase · 🔴 conflict found between the RFC and the real code.


1. Endpoints — served

1a. contact-service (this RFC builds/extends these)

EndpointMethodAuthN/AuthZRequest schemaResponse schemaStatus codesIdempotencyOwning task(s)Covers AC(s)
/iag/v1/contacts/{contact_id}/notesPOSTRequirePermissionMiddleware + customers_customernotes_add; owner from X-Authenticated-Userid🟢 CreateContactNoteRequest{note, attachments?} (payload/contact_notes_request.go:8-11)🟢 CreateContactNoteResponse{contact_note_id, created_at, updated_at} (:33-37) 🟡 + mentioned_user_ids, mentioned_users (proposed)200, 400, 401, 403, 422 (TOO_MANY_MENTIONS/NOTE_TOO_LONG), 500none (create)BE: 2.1, 2.2, 2.3, 2.4, 2.5 · FE (writes anchor): 1.1 · FE (reads response): 2.7S01/AC-1..AC-4, ERR-1, ERR-2; S02/AC-1..AC-4, ERR-1; S04/AC-1, ERR-1..ERR-3; S06-NEG/NEG-1, NEG-2
/iag/v1/contacts/{contact_id}/notes/{contact_note_id}PUT+ customers_customernotes_manage; owner-only🟢 UpdateContactNoteRequest{note, attachments?} (:14-17)🟢 ContactNoteResponse{id, contact_id, note, attachments, owner_id, owner_name, permission, created_at, updated_at} (:20-30) 🟡 + mentioned_user_ids, mentioned_users (proposed)200, 400, 401, 403, 404, 422, 500last-write-wins (no version field)BE: 2.1, 2.2, 2.3, 2.4, 2.5 · FE: 1.1, 2.7same as POST plus S01/AC-4, S02/AC-4, S03/AC-3 (edit-path idempotent notify)
/iag/v1/contacts/{contact_id}/notesGET+ customers_customernotes_view🟢 GetContactNotesRequest{page, per_page, order_by, order_direction, owner_ids?} (:40-47)🟢 GetContactNotesResponse{notes: ContactNoteResponse[], meta: PaginationMeta} (:50-61) 🟡 each note item + mentioned_user_ids, mentioned_users (proposed)200, 401, 403, 500n/an/a — reused, unchanged. FE render consumer: 1.2S01/AC-2 (chip renders from the anchor this endpoint returns)

Note path reality (RFC-verified): the backend serves both the canonical /iag/v1/contacts/{contact_id}/notes and a deprecated /iag/v1/contacts/notes/{contact_id} group, routed to the same handler (RFC OQ-15) — a response-shape change here appears on both automatically. No per-group work needed.

1b. Launchpad (platform-owned; consumed FE-direct or BE-relayed, not built by this RFC)

EndpointMethodAuthN/AuthZRequest schemaResponse schemaStatus codesOwning task(s)Covers AC(s)
Company user list (typeahead)GETsession auth🟢 GET {CUSTOMER_360_URL}/v1/users?order_by=&order_direction=&page=&per_page=&statuses=active (FE-direct, qontak-customer-fe/.../UserStore.ts:100) — not a direct call to Launchpad's own /private/users🟢 { data: [{ Pagination: {page,per_page,total}, UserResponse: User[] }] }, User{id,email,full_name,status,current_sign_in_at,sso_id,created_at,user_access,role} (UserStore.ts:11-19) — confirmed no avatar field200, 5xxFE: 1.1 (mocked) → 2.7 (real, client-side filter)S01/AC-1, ERR-1, ERR-2
Single-user detail (preview card)GETsession auth (BE-relayed via contact-service's own Launchpad client)🟢 GET {Launchpad root}/private/users/get_by_sso_id?sso_id={uuid} (contact-service/internal/app/api/qontak_launchpad.go:223-238)🟢 LaunchpadUserDetailResponse{full_name,email,sso_id,nik,phone,role_crs_id,staff_level,status,external_company_id,unified_app} (qontak_launchpad.go:545-556) — confirmed no teams[], no avatar (matches OQ-22); has more real fields than the RFC's simplified subset (see §4.3)200, 404 (generic — OQ-20), 5xxFE: 1.4 (mocked) → 2.8 (real, session-cached)S05/AC-1, AC-2, ERR-1, ERR-2
Bulk user names lookup (mentioned_users email snapshot, Decision 15)(bulk, exact HTTP verb/path not yet re-grounded here)service auth (BE-internal — contact-service's own Launchpad client)🟡 IUserService.GetUserNamesBulk(ctx, ssoIDs) — an already-existing client method per the RFC's OQ-13 research, distinct from GetUsersByUserSsoIds (used by GetActiveUsersBySsoIds, §7); called in addition to that active-membership check because it alone returns only SsoID+Status, no email🟡 []{sso_id, full_name, email} (per OQ-13; exact struct/file:line not cited in the RFC — flag for re-grounding, see §8)assume 200, 5xx (unverified)BE: 2.5S01/AC-3 (mentioned_users persisted alongside mentioned_user_ids)

2. Endpoints — depended on (outbound calls contact-service makes)

CallMethodAuthRequest (per the RFC)TimeoutRetryFailure behaviorOwning task(s)Covers AC(s)
POST /api/v1/notifications/crm (notification-service)POSTX-Api-Key (service, constant-time compared)🟡 proposed — see §4.4 (net-new client, no notification_client.go exists in contact-service today — confirmed by repo search)10s3× exponential backoff (1,4,16,64s per the worker's backoff sequence)log cdp_note_mention_notify_failed; note stays saved (never blocks the write)BE: 2.6S03/AC-1, AC-2, AC-2b, AC-3, ERR-1, ERR-2

3. Inbound webhooks

N/A — no inbound webhook. (verbatim from the RFC — notification dispatch is outbound only; mark-as-read and rendering are owned by the Notification Center / mobile app, not contact-service.)


4. Request/Response JSON contract

Every example below is either 🟢 shaped exactly like the real, currently-shipping payload in contact-service / qontak-customer-fe (mention-related fields aside, since those don't exist yet), or 🟡 explicitly marked proposed where this RFC adds something net-new. Grounding citations (file:line) are kept so the shapes are checkable, but the source struct/interface definitions themselves are not reproduced here — this is a contract, not a technical design doc. The response envelope (resp_code/resp_desc/meta/data) is real, taken from internal/pkg/http/response.go:1-50 and internal/pkg/http/handler.go:26-29 — the RFC's own Detail 2.4 table omitted this envelope and showed only the inner data payload; don't skip it when implementing.

4.1 POST /iag/v1/contacts/{contact_id}/notes

🟢 grounded against payload/contact_notes_request.go:8-11 (request) and :33-37 (response) + internal/app/repository/contact_notes/base.go:17-23 (Attachment shape).

Request

{
"note": "<p>Following up with the client. cc <span data-type=\"mention\" data-id=\"3f2504e0-4f89-41d3-9a0c-0305e82c3301\" data-label=\"Budi Santoso\">@Budi Santoso</span></p>",
"attachments": [
{
"url": "https://cdn.qontak.com/notes/attachments/abc123.pdf",
"type": "pdf",
"file_size_in_byte": 204800,
"file_size": "200 KB",
"file_name": "proposal.pdf"
}
]
}

Response (mentioned_user_ids 🟡 proposed — Decision 1; mentioned_users 🟡 proposed — Decision 15; not yet on the real CreateContactNoteResponse, which today returns only contact_note_id/created_at/updated_at. A mention dropped for being invalid/cross-company (Decision 5) is silent — logged server-side only, not returned here.)

{
"resp_code": "200",
"resp_desc": { "id": "OK", "en": "OK" },
"meta": { "version": "d2b6e92", "api_env": "production", "trace_id": "6a476a000000000085019557e6ec6082" },
"data": {
"contact_note_id": "66f1a2b3c4d5e6f7a8b9c0d1",
"created_at": "2026-07-09T10:15:00+07:00",
"updated_at": "2026-07-09T10:15:00+07:00",
"mentioned_user_ids": ["3f2504e0-4f89-41d3-9a0c-0305e82c3301"],
"mentioned_users": [
{ "id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301", "email": "budi.santoso@qontak.com" }
]
}
}

4.2 PUT /iag/v1/contacts/{contact_id}/notes/{contact_note_id}

🟢 grounded against payload/contact_notes_request.go:14-17 (request, identical shape to POST) and :20-30 (response — full ContactNoteResponse, real fields).

Request (editing the note to add a second mention)

{
"note": "<p>Following up with the client. cc <span data-type=\"mention\" data-id=\"3f2504e0-4f89-41d3-9a0c-0305e82c3301\" data-label=\"Budi Santoso\">@Budi Santoso</span> and <span data-type=\"mention\" data-id=\"7c1a9e22-6b3d-4a10-9e5f-1122334455aa\" data-label=\"Siti Rahayu\">@Siti Rahayu</span></p>",
"attachments": []
}

Response (mentioned_user_ids 🟡 proposed — Decision 1; mentioned_users 🟡 proposed — Decision 15)

{
"resp_code": "200",
"resp_desc": { "id": "OK", "en": "OK" },
"meta": { "version": "d2b6e92", "api_env": "production", "trace_id": "6a476a000000000085019557e6ec6083" },
"data": {
"id": "66f1a2b3c4d5e6f7a8b9c0d1",
"contact_id": "66e0a1b2c3d4e5f6a7b8c9d0",
"note": "<p>Following up with the client. cc <span data-type=\"mention\" data-id=\"3f2504e0-4f89-41d3-9a0c-0305e82c3301\" data-label=\"Budi Santoso\">@Budi Santoso</span></p>",
"attachments": [],
"owner_id": "9d8e7f6a-5b4c-3d2e-1f0a-9b8c7d6e5f4a",
"owner_name": "Siti Aminah",
"permission": { "update": true, "delete": true },
"created_at": "2026-07-09T10:15:00+07:00",
"updated_at": "2026-07-09T11:02:00+07:00",
"mentioned_user_ids": ["3f2504e0-4f89-41d3-9a0c-0305e82c3301"],
"mentioned_users": [
{ "id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301", "email": "budi.santoso@qontak.com" }
]
}
}

4.3 GET /iag/v1/contacts/{contact_id}/notes

🟢 grounded against payload/contact_notes_request.go:40-47 (request query params) and :50-61 (response).

Requestcontact_id comes from the URL path; everything else is a query param, no JSON body:

GET /iag/v1/contacts/66e0a1b2c3d4e5f6a7b8c9d0/notes?page=1&per_page=10&order_by=created_at&order_direction=desc

Response (mentioned_user_ids/mentioned_users on each note 🟡 proposed — Decisions 1 and 15)

{
"resp_code": "200",
"resp_desc": { "id": "OK", "en": "OK" },
"meta": { "version": "d2b6e92", "api_env": "production", "trace_id": "6a476a000000000085019557e6ec6084" },
"data": {
"notes": [
{
"id": "66f1a2b3c4d5e6f7a8b9c0d1",
"contact_id": "66e0a1b2c3d4e5f6a7b8c9d0",
"note": "<p>...cc <span data-type=\"mention\" data-id=\"3f2504e0-4f89-41d3-9a0c-0305e82c3301\" data-label=\"Budi Santoso\">@Budi Santoso</span></p>",
"attachments": [],
"owner_id": "9d8e7f6a-5b4c-3d2e-1f0a-9b8c7d6e5f4a",
"owner_name": "Siti Aminah",
"permission": { "update": true, "delete": true },
"created_at": "2026-07-09T10:15:00+07:00",
"updated_at": "2026-07-09T11:02:00+07:00",
"mentioned_user_ids": ["3f2504e0-4f89-41d3-9a0c-0305e82c3301"],
"mentioned_users": [
{ "id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301", "email": "budi.santoso@qontak.com" }
]
}
],
"meta": { "total": 1, "limit": 10, "offset": 0, "has_more": false }
}
}

⚠️ Two different meta keys are in play — the outer envelope's meta (version/api_env/ trace_id) and this endpoint's own pagination object nested inside data.meta (total/limit/offset/has_more). Don't conflate them when implementing the FE client.

4.4 Launchpad — Company user list (FE-direct)

🟢 grounded against qontak-customer-fe/features/customers/store/UserStore.ts:11-19,83-129 — this call goes straight from FE to CUSTOMER_360_URL, not through contact-service.

Request

GET {CUSTOMER_360_URL}/v1/users?order_by=created_at&order_direction=desc&page=1&per_page=50&statuses=active

Response

{
"data": [
{
"Pagination": { "page": 1, "per_page": 50, "total": 2 },
"UserResponse": [
{
"id": "usr_101",
"email": "budi.santoso@qontak.com",
"full_name": "Budi Santoso",
"status": "active",
"current_sign_in_at": "2026-07-08T09:00:00Z",
"sso_id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"created_at": "2025-01-10T00:00:00Z",
"user_access": "member",
"role": "agent"
}
]
}
]
}

🔴 Resolved conflict: the previous draft of this contract flagged avatar presence as an unresolved conflict inside the RFC (Detail 2.4/2.G said present-and-nullable; §1 Dependencies + Decision 11 said absent). Reading the real User interface FE actually declares and consumes settles it: there is no avatar field anywhere in this response. §1 Dependencies and Decision 11 are correct; Detail 2.4's endpoint table and Detail 2.G are wrong on this point and should be corrected in the RFC.

4.5 Launchpad — Single-user detail (preview card)

🟢 grounded against contact-service/internal/app/api/qontak_launchpad.go:223-238 (request) and :545-556 (response struct) + internal/app/api/response.go:18-29 (envelope).

Request

GET {Launchpad root}/private/users/get_by_sso_id?sso_id=3f2504e0-4f89-41d3-9a0c-0305e82c3301
Authorization: <service token>
Content-Type: application/json
X-Authenticated-Userid: 3f2504e0-4f89-41d3-9a0c-0305e82c3301

Response

{
"resp_code": "200",
"resp_desc": { "id": "OK", "en": "OK" },
"meta": { "version": "d2b6e92", "api_env": "production" },
"data": {
"full_name": "Budi Santoso",
"email": "budi.santoso@qontak.com",
"sso_id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"nik": "3201012345670001",
"phone": "+6281234567890",
"role_crs_id": "crs_001",
"staff_level": "senior",
"status": "active",
"external_company_id": 4821,
"unified_app": true
}
}

The preview card only consumes full_name/email/staff_level/status/sso_id from this response — the rest (nik/phone/role_crs_id/external_company_id/unified_app) are real wire fields the RFC's simplified schema never mentioned; harmless to ignore. Confirmed: no teams, no avatar anywhere in the real response — this matches the RFC's OQ-22 claim exactly (unlike the avatar conflict in §4.4, this one checks out).

4.6 POST /api/v1/notifications/crm

🟡 Fully proposed — contact-service has no notification client today (confirmed: no file for it and no reference to this endpoint path exists anywhere in the repo). Field names below are per the RFC's claims against notification-service's handler, which is not checked out locally and so is unverified here — treat this section as RFC-only, not code-verified, until that repo is available for cross-check. BE-internal only (contact-servicenotification-service) — no FE consumer.

Request

{
"sso_id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"title": "Siti Aminah mentioned you in a note",
"description": "On contact Budi Santoso",
"notif_type": "1",
"notif_category": "2",
"click_action": "OPEN_URL",
"click_action_url": "https://app.qontak.com/customers/66e0a1b2c3d4e5f6a7b8c9d0?tab=notes",
"organization_id": "b6a8c1d2-3e4f-5061-7283-940a1b2c3d4e"
}

Auth header: X-Api-Key. No skip_fcm field exists — FCM fan-out is automatic for crm-origin notifications (see §6 for the lock-screen PII consequence, OQ-19). No documented response body — dispatch is fire-and-forget from contact-service's side (see §5 for the failure path: retry×3, then log and move on).


5. Error & status code catalog

CodeHTTP statusTrigger conditionEndpoint(s)Emitted byConsumed byCovers AC(s)
NOTE_TOO_LONG422HTML > 10,000 charsPOST/PUT .../notesBEFE ("Note is too long", inline)
TOO_MANY_MENTIONS422> 10 distinct mentionsPOST/PUT .../notesBEFE (inline validation)Decision 6
BAD_REQUEST400malformed bodyPOST/PUT .../notesBEFE
FORBIDDEN403non-owner updatePUT .../notes/{id}BEFE
NOT_FOUND404wrong contact/company.../notes/{id}BEFE
cdp_note_mention_invalid (log event, not an HTTP code — soft-fail, 200)200invalid / cross-company / 404-deleted SSO id, silently dropped (Decision 5)POST/PUT .../notesBEops/audit logs only — no FE surfaceS04/ERR-1, ERR-3; S06-NEG/NEG-1
cdp_note_mention_notify_failed (log event, not an HTTP code)n/a — internalnotification-service 5xx/timeout, retry×3 exhaustedPOST /api/v1/notifications/crm (dependency)BE workerops alerting (#cdp-ops, rate > 5%)S03/ERR-1
generic 404 (Launchpad)404unresolved user — deleted or non-existent, indistinguishable (OQ-20)Company user list; Single-user detailLaunchpadFE ("No matching people" / inactive-not-found card)S01/ERR-2; S04/ERR-3; S05/ERR-1
5xx/timeout (Launchpad detail)5xxlookup failureSingle-user detailLaunchpadFE ("Couldn't load details")S05/ERR-2

🟢 The BE error envelope shape (resp_code/resp_desc: {id, en}/meta) is real — internal/pkg/http/response.go:12-16. Real error JSON:

{
"resp_code": "422",
"resp_desc": { "id": "Catatan melebihi batas 10.000 karakter", "en": "Note content exceeds maximum length of 10,000 characters" },
"meta": { "version": "d2b6e92", "api_env": "production", "trace_id": "6a476a000000000085019557e6ec6085" }
}

6. UI contract (FE)

Web

  • DOM/data-attribute contract: mention span <span data-type="mention" data-id="{sso_uuid}" data-label="{full_name}">@{full_name}</span>. DOMPurify config ADD_ATTR: ['data-type','data-id','data-label'] required at both save (NoteInput.vue:460) and render (NotesList.vue:104) — without it, mention identity is silently stripped. (span is already in DOMPurify's default allow-listed tags, so no ADD_TAGS is needed.) (Owning tasks: 1.1, 1.2)
  • State ownership: Pinia CustomerStore (note CRUD, reads mentioned_user_ids) + UserStore (typeahead source + getUserDetail(ssoId) for the preview card — 🟢 real store confirmed at qontak-customer-fe/features/customers/store/UserStore.ts, though getUserDetail doesn't exist there yet, only getUsers/getTeams/getPermission); local noteValue ref in NoteInput.vue. (Owning tasks: 2.7, 2.8)
  • Analytics events:
    • cdp_note_mention_added — note created or edited with ≥1 mention. Payload: note_id, contact_id, company_sso_id, mention_count, new_mention_count, author_sso_id, event (event{create, edit}; mention_count = total on the note, new_mention_count = |to_notify| this save).
    • cdp_note_mention_picker_failed — typeahead/user-list call failed. Payload: company_sso_id, platform.
    • cdp_note_mention_preview_failed (RFC-local, FE-only, S05 — not in the PRD's §11) — preview-card lookup failed (5xx/timeout). Payload: company_sso_id, sso_id, platform. (Owning task: 2.8)
  • A11y: typeahead listbox role="listbox"/option, arrow-key nav, aria-activedescendant; mention chip role="link", accessible name @{full_name}; preview card keyboard-focusable from the chip, Esc closes.

Mobile

  • Anchor emitted: <a href="../../../users/{sso_uuid}/edit_user">@{full_name}</a> (Decision 8). (Owning task: mobile mapper change, tracked in the RFC's Detail 4.D chunk 8 — no corresponding task-breakdown id, since this breakdown covers BE + web FE only.)
  • Render: plain-text @Name fallback only (Decision 7) — no chip, no preview card in v1.

7. Field-name parity check

FE expectsBE/platform response hasEndpointVerdict
mentioned_user_idsmentioned_user_ids (🟡 proposed — not yet on the real ContactNoteResponse/CreateContactNoteResponse structs)POST/PUT .../notes✅ shape agreed — ⚠️ not yet built: the field doesn't exist in contact-service today (confirmed by reading base.go + contact_notes_request.go); Tasks 2.1/2.5 (BE) and 2.7 (FE) add it
mentioned_users🟡 proposed — Decision 15; not yet on the real structsPOST/PUT .../notes + GET (each note item)✅ shape agreed — ⚠️ not yet built (Task 2.5); no FE consumer scoped yet (Decision 15's own stated consequence) — available on the wire as a future display fallback for the preview card (Decision 14) if/when FE wires it up; sourced from a second Launchpad call (GetUserNamesBulk), not the active-membership call, which returns no email (OQ-13)
data-id mention span (parser input)composer emits it (Task 1.1)POST/PUT .../notes✅ match only if DOMPurify ADD_ATTR ships on both save and render (Task 1.1/1.2) — otherwise the attribute is stripped before the parser ever sees it
mobile href SSO form (parser input)mobile mapper emits id, not sso_id, until the mobile CDP-mapper change landsPOST/PUT .../notes⚠️ must ship together — RFC Detail 2.G flags this explicitly: until the mapper change lands, mobile emits the wrong id and the backend would store a non-SSO value
teams (preview card)LaunchpadUserDetailResponse (real struct, qontak_launchpad.go:545-556) has no teams[] fieldSingle-user detail⚠️ confirmed real gap, OQ-22 — verified against actual code, not just RFC prose; card ships without the teams row unless resolvable via getTeams() client-side mapping
avatar (typeahead picker)RESOLVED — real User interface (UserStore.ts:11-19) has no avatar field. Detail 2.4's endpoint table and Detail 2.G (which claimed it's present-and-nullable) are wrong; §1 Dependencies + Decision 11 (which claimed it's absent) are correct.Company user list🔴→✅ was an open conflict, now settled by code — recommend the RFC author correct Detail 2.4/2.G, not build a null-check for a field that never arrives
notification title/descriptionserver-constructed only, never user note HTMLPOST /api/v1/notifications/crm✅ verified clean — RFC Detail 2.G rates this "yes (low risk)"; no user-controlled markup reaches the notification body
click_action_url → notes tabhost/MFE doesn't parse ?tab=notes yet, and the Notes tab is gated behind isStagingweb deep-link (via Notification Center)⚠️ open, OQ-17 — two small qontak-customer-fe changes still pending

No other FE-read field in sections 4/6 was found without a corresponding entry above.


8. Open items

  • mentioned_users (Decision 15) needs its own grounding pass. §1b's new "Bulk user names lookup" row and §4's response examples are marked 🟡 proposed off the RFC's OQ-13 research (GetUserNamesBulk "already exists" and returns names, but discards Status) — this contract could not independently verify the method's exact signature, HTTP path, or response field names against contact-service (no sibling checkout consulted for this pass). Confirm before Task 2.5 starts. mentioned_users[].email is also directly identifying PII (a materially higher classification than the pseudonymous mentioned_user_ids) with no FE consumer wired up yet — it sits under the same infosec approver gate as the rest of this RFC's mention work (RFC Detail 3.D).
  • teams[] / avatar on the single-user preview-card detail response — open (OQ-22), non-blocking, confirmed against real code (§4.5/§7): the actual LaunchpadUserDetailResponse struct has neither field. Ships degraded (name/email/staff level only) unless a client-side getTeams() mapping resolves teams.
  • avatar on the company user list — now resolved, action needed on the RFC, not the build. §4.4/§7 confirmed via the real FE User interface that avatar never arrives. Recommend a one-line fix to the RFC's Detail 2.4 endpoint table and Detail 2.G row rather than building FE null-check logic for a field that doesn't exist.
  • Task-breakdown story-id drift: the task breakdown's Task 2.5 cites story id NOTE-MENTION-S05-NEG, but the RFC's v1.6 sync renumbered this guard-rail story to S06-NEG (see RFC lines 98-99, Detail 1.B row 20, Detail 1.C). The task breakdown was not regenerated after the renumbering — its S05-NEG references in Task 2.5's title and ACs are stale and should read S06-NEG. Recommend re-running rfc-task-breakdown (or a manual fix) before relying on that task's story tag.
  • POST /api/v1/notifications/crm request shape (§4.6) is unverified against real codenotification-service is not checked out locally; the RFC's own claims are grounded against that repo (per its provenance notes), but this contract could not independently confirm them. Re-run the grounding check if/when that repo becomes available as a sibling checkout.
  • OQ-1 (non-blocking) — reuse the generic mention(2) notification category vs. add a CDP-specific one; owned by CDP + Notification Platform.
  • OQ-14 (the RFC's sole true blocker) — Figma frames for the web typeahead picker + chip don't exist yet; gates the visual half of Task 1.1/1.2 only (the anchor/sanitize logic is buildable now).
  • OQ-17 (open, small) — web deep-link ?tab=notes parsing + un-gating the Notes tab from isStaging; tracked as its own chunk in the RFC's Detail 4.D (chunk 10), no task-breakdown id since it's independent FE work.
  • OQ-19 (infosec ack pending) — auto-FCM fan-out puts the contact name on a mobile lock-screen push with no skip_fcm opt-out; needs explicit infosec acceptance before AGREED, or a generic-description mitigation. Does not block build.
  • Mobile anchor + backend dual-parse sequencing — per §7's parity check, the mobile mapper change and the backend's dual-form parser (Task 2.2) must ship together; shipping one without the other stores the wrong id type from mobile notes.