Skip to content

[Bug Report: API Field Missing] gender cannot be set or changed on an existing contact β€” upsert accepts it, returns 200, and silently discards itΒ #313

Description

@brumiser1550

πŸ› Describe the Bug

There is no API path to set or correct gender on a contact that already exists.
The two endpoints that could do it fail in different ways, and one of them fails
silently.

PUT /contacts/{contactId} rejects it β€” honestly.

PUT /contacts/{contactId}   { "gender": "male" }
-> 422 { "message": ["property gender should not exist"],
         "error": "Unprocessable Entity", "statusCode": 422 }

This matches the spec: UpdateContactDto does not declare gender. No
complaint about this part β€” the error is clear and correct.

POST /contacts/upsert accepts it and throws it away.

UpsertContactDto does declare gender. On an existing contact the call
returns 200, reports "new": false, and echoes back the stored value
rather than the submitted one. The submitted value is discarded with no error,
no warning, and no indication anything was ignored.

Verified two ways:

// (a) changing an existing value
POST /contacts/upsert  { "locationId": "...", "email": "x@example.com",
                         "firstName": "AST", "gender": "male" }
// -> 200 { "new": true,  "contact": { "gender": "male" } }
GET /contacts/{id}     // -> "gender": "male"

POST /contacts/upsert  { "locationId": "...", "email": "x@example.com",
                         "gender": "female" }
// -> 200 { "new": false, "contact": { "gender": "male" } }   <-- submitted "female", echoed "male"
GET /contacts/{id}     // -> "gender": "male"                 <-- unchanged

// (b) setting it for the first time on a contact created without it
POST /contacts/        { "locationId": "...", "email": "y@example.com" }
GET /contacts/{id}     // -> "gender": null

POST /contacts/upsert  { "locationId": "...", "email": "y@example.com",
                         "gender": "female" }
// -> 200 { "new": false, "contact": { "gender": null } }
GET /contacts/{id}     // -> "gender": null                   <-- still unset

So gender is effectively write-once, at creation only. Once a contact
exists it can never be set, corrected, or cleared through the API.

For contrast, UpdateContactDto omits three fields relative to
CreateContactDto: locationId, gender, and companyName. locationId has
an obvious reason to be immutable β€” it is the contact's owning location, and
changing it would be a move rather than an edit. gender is a plain scalar
attribute with no equivalent rationale, and nothing in the documentation says it
is create-only.

πŸ“ API Endpoint

POST /contacts/upsert, PUT /contacts/{contactId}

βœ… Expected Behavior

Any one of these would resolve it, in descending order of preference:

  1. Honour gender on POST /contacts/upsert when new: false. It is
    already declared in UpsertContactDto, so this is the behaviour the schema
    already promises.
  2. Add gender to UpdateContactDto so PUT /contacts/{contactId} can set
    it, consistent with every other scalar contact attribute.
  3. At minimum: document that gender is create-only, and have upsert
    reject it on an existing contact the way PUT does, instead of returning
    200 and discarding it. A documented field that is silently ignored is worse
    than one that errors.

πŸ’» Screenshots or Code Samples

Minimal reproduction β€” no setup beyond a sub-account:

// 1. create with a gender
POST /contacts/upsert
{ "locationId": "{locationId}", "email": "repro@example.com",
  "firstName": "Repro", "gender": "male" }
// -> 200 { "new": true, "contact": { "id": "{contactId}", "gender": "male" } }

// 2. try to change it β€” succeeds by status code, does nothing
POST /contacts/upsert
{ "locationId": "{locationId}", "email": "repro@example.com", "gender": "female" }
// -> 200 { "new": false, "contact": { "gender": "male" } }

// 3. confirm
GET /contacts/{contactId}
// -> { ..., "gender": "male" }

// 4. the honest failure, for comparison
PUT /contacts/{contactId}  { "gender": "female" }
// -> 422 { "message": ["property gender should not exist"] }

Product Area

contacts

πŸ“‹ Use Case

We sync contact records from external systems into GoHighLevel sub-accounts on a
schedule. A sync resolves the existing contact, diffs it against the incoming
record, and writes what changed.

Because gender can only be written at creation:

  • Contacts we adopt rather than create β€” ones that already existed in the
    sub-account β€” can never have gender populated at all.
  • If a source system corrects a contact's gender, we cannot propagate the
    correction.
  • We spent time building an upsert-based workaround for exactly this, because
    the 200 response gave every indication it had worked. It had not.

We are not able to use POST /contacts/upsert as a general substitute for
update in any case: per #37 it resolves which contact to write from the
sub-account's duplicate-contact settings, so when an email matches one contact
and a phone matches another it updates whichever ranks first. When we have
already resolved a contact by id, we need an endpoint that writes to that id.

🚨 Why Should This Be Prioritized?

The silent-discard is the core of it. A 200 with the field echoed back at its
old value is indistinguishable from success unless you re-read and compare β€”
which is not something most integrations do after a write that reported OK.

Any integration that believes it is keeping gender in sync is silently wrong,
and will stay wrong indefinitely. If the field genuinely cannot be updated, a
422 β€” which PUT already returns for the same field β€” would surface that
immediately instead.

🧠 Additional Context

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions