feat(chat): add updateContact endpoint to set a contact's name by luantaraschi · Pull Request #2758 · evolution-foundation/evolution-api · GitHub
Skip to content

feat(chat): add updateContact endpoint to set a contact's name - #2758

Open
luantaraschi wants to merge 1 commit into
evolution-foundation:developfrom
luantaraschi:feat/update-contact
Open

luantaraschi wants to merge 1 commit into
evolution-foundation:developfrom
luantaraschi:feat/update-contact

Conversation

@luantaraschi

@luantaraschi luantaraschi commented Oct 15, 2026 •

Copy link
Copy Markdown

📋 Description

Adds POST /chat/updateContact/{instance} for Baileys instances. It calls Baileys' addOrEditContact, which sends a contactAction app-state patch, so a name set through the API is written to the account's WhatsApp contact list instead of staying only in the application's database. The request follows the shape proposed in #2740:

{
  "number": "5546999103009",
  "fullName": "Ricardo Cavalcante | Ortopedista",
  "firstName": "Ricardo",
  "saveOnPrimaryAddressbook": false
}

number takes a phone number or a JID (@s.whatsapp.net or @lid). firstName and saveOnPrimaryAddressbook are optional and are only put in the action when given. Groups, newsletters and broadcasts get a 400.

The phone number JID goes in the patch index and the LID goes in contactAction.lidJid. That's the layout of contact actions that come from the phone: Baileys' processContactAction reads the PN from index[1] and notes that contactAction.pnJid is usually null. Whichever JID is missing is resolved through signalRepository.lidMapping and normalized, since getPNForLID returns a device JID (...:0@s.whatsapp.net). If a LID is passed and no phone number is known for it, the endpoint returns 400 rather than sending a patch indexed by the LID.

Evolution runs Baileys with emitOwnEvents: false, so the change is not echoed back. Once the patch is accepted, the method runs the existing contacts.update handler for the PN JID (Contact row and CONTACTS_UPDATE webhook) and also renames the LID Contact row if one exists, so findContacts stops returning the old name.

I left removeContact out. I wasn't sure what the local Contact row should show after a removal, and it can be its own change.

🔗 Related Issue

Closes #2740

🧪 Type of Change

  • 🐛 Bug fix (non-breaking change which fixes an issue)
  • ✨ New feature (non-breaking change which adds functionality)
  • 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • 📚 Documentation update
  • 🔧 Refactoring (no functional changes)
  • ⚡ Performance improvement
  • 🧹 Code cleanup
  • 🔒 Security fix

🧪 Testing

I have not tried this against a paired phone. #2711/#2712 attempted the same feature in August, and its author closed it after reporting that the server acknowledged the patch but the name did not show up on the phone. This PR differs from #2712 in a few places: the action leaves pnJid unset, does not force saveOnPrimaryAddressbook: true, and only one patch is sent (no second one indexed by the LID). I don't know whether any of that changes what the device does. One data point: WAHA had the same report (devlikeapro/waha#2285, the API returns true and the phone doesn't change), and the fix they shipped in 2026.9.2 (devlikeapro/waha@235ec01) adds the normalized LID as lidJid with the PN in the index, which is the action this PR sends. The reporter there hasn't confirmed it yet. @biancastronco, you described this exact setup in #2740; if you can try the branch on your instance, that would settle it.

What I ran, on Node 20 as in the CI workflow:

  • npm run lint:check, npm run db:generate and tsc --noEmit are clean.

  • tsup runs out of memory with the default heap in my 8 GB container, on a clean develop as well. With --max-old-space-size=6144 it builds.

  • There is no test suite, so I called updateContact with a stubbed client and repository (tsx on Node 24, which the runtime needs). 28 checks: PN with and without a known LID, LID with and without a known PN, a PN that the onWhatsApp cache resolves to a LID, group JID, number not on WhatsApp, Baileys throwing (500, nothing written locally), contact persistence turned off, the patch Baileys builds from the action (index: ['contact', <pn>], critical_unblock_low), and the request schema. I didn't commit the script because there is no runner to hook it into. I can add it under test/ if you want it.

  • Manual testing completed

  • Functionality verified in development environment

  • No breaking changes introduced

  • Tested with different connection types (if applicable)

✅ Checklist

  • My code follows the project's style guidelines
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have manually tested my changes thoroughly
  • I have verified the changes work with different scenarios
  • Any dependent changes have been merged and published

POST /chat/updateContact/{instance} sends Baileys' addOrEditContact
(app-state contactAction) so a name set by the API is written to the
account's WhatsApp contact list instead of living only in our database.

The patch is indexed by the phone number JID with the LID inside the
action, the same layout Baileys receives when the phone syncs a
contact. Both JIDs are resolved through the LID mapping, so either one
can be passed as `number`. The local Contact rows (PN and LID) and the
CONTACTS_UPDATE webhook are refreshed through the existing
contacts.update handler, since emitOwnEvents is off.

Refs evolution-foundation#2740
@sourcery-ai

sourcery-ai Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 5 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts" line_range="4602" />
<code_context>
+    }
+
+    // emitOwnEvents is off, so Baileys will not echo this change back to us.
+    await this.contactHandle['contacts.update']([{ id: pnJid, name: data.fullName }]);
+
+    // Chats that arrived addressed to the LID keep their own contact row.
</code_context>
<issue_to_address>
**Accepted updates leave contacts stale**

When baileys accepts the patch, then the profile-picture lookup, event adapter, or PN contact write in `contacts.update` fails, `updateContact` propagates the `contacts.update` failure after `addOrEditContact` has accepted the WhatsApp change, so the API returns an error while the local contact remains stale; a profile-picture lookup failure also prevents the webhook from being sent.

Make local synchronization failure-safe after the remote patch, retrying or reconciling failed contact updates instead of leaving the accepted change stale.
</issue_to_address>

### Comment 2
<location path="src/validate/chat.schema.ts" line_range="327" />
<code_context>
+  type: 'object',
+  properties: {
+    number: { type: 'string' },
+    fullName: { type: 'string' },
+    firstName: { type: 'string' },
+    saveOnPrimaryAddressbook: { type: 'boolean' },
</code_context>
<issue_to_address>
**Long names fail local persistence**

When `fullName` exceeds 100 characters and contact persistence is enabled, the schema accepts the name, but the Contact `pushName` column is limited to 100 characters. Baileys accepts the patch before Prisma rejects the local write, so the request fails and the local contact remains stale.

Set a 100-character maximum for `fullName` in `updateContactSchema`.
</issue_to_address>

### Comment 3
<location path="src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts" line_range="4602" />
<code_context>
+    }
+
+    // emitOwnEvents is off, so Baileys will not echo this change back to us.
+    await this.contactHandle['contacts.update']([{ id: pnJid, name: data.fullName }]);
+
+    // Chats that arrived addressed to the LID keep their own contact row.
</code_context>
<issue_to_address>
**Saved avatars disappear**

When a contact has a saved avatar and the profile-picture lookup during a name update fails or returns null, `contacts.update` writes the null `profilePicture` result into `profilePicUrl`, replacing the saved avatar, so contact listings lose it.

Preserve the existing `profilePicUrl` when the lookup fails or returns null.
</issue_to_address>

### Comment 4
<location path="src/validate/chat.schema.ts" line_range="332" />
<code_context>
+    saveOnPrimaryAddressbook: { type: 'boolean' },
+  },
+  required: ['number', 'fullName'],
+  ...isNotEmpty('number', 'fullName'),
+};
+
</code_context>
<issue_to_address>
**Empty names are accepted**

When the request includes an unrecognized property or an optional field and `fullName` is empty, `isNotEmpty` skips the `minLength` check when a property name is outside its enum, so the empty `fullName` reaches `addOrEditContact` and leaves the contact with a blank name.

Validate that `fullName` is nonempty independently of the other request properties.
</issue_to_address>

### Comment 5
<location path="src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts" line_range="4602-4610" />
<code_context>
+    }
+
+    // emitOwnEvents is off, so Baileys will not echo this change back to us.
+    await this.contactHandle['contacts.update']([{ id: pnJid, name: data.fullName }]);
+
+    // Chats that arrived addressed to the LID keep their own contact row.
+    if (lidJid && this.configService.get<Database>('DATABASE').SAVE_DATA.CONTACTS) {
+      await this.prismaRepository.contact.updateMany({
+        where: { remoteJid: lidJid, instanceId: this.instanceId },
+        data: { pushName: data.fullName },
+      });
+    }
+
+    return { update: 'success', remoteJid: pnJid, lid: lidJid, fullName: data.fullName };
</code_context>
<issue_to_address>
**PN and LID names diverge**

When the PN update succeeds but the separate LID contact update fails, the PN row is committed while the LID row retains the old name, so LID-based contact reads remain stale and the caller receives an error despite the accepted WhatsApp patch.

Make the PN and LID contact updates atomic, or retry/reconcile a failed LID update.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 5 findings to address first, and the endpoint writes the supplied name to WhatsApp’s contact state and updates matching local contact rows, so reverting the code would not restore the previous name. The affected contacts are bounded, and their names can be corrected with another update.

Blocking findings: src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts:4602, src/validate/chat.schema.ts:327, src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts:4602, src/validate/chat.schema.ts:332, src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts:4610


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

}

// emitOwnEvents is off, so Baileys will not echo this change back to us.
await this.contactHandle['contacts.update']([{ id: pnJid, name: data.fullName }]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Accepted updates leave contacts stale

When baileys accepts the patch, then the profile-picture lookup, event adapter, or PN contact write in contacts.update fails, updateContact propagates the contacts.update failure after addOrEditContact has accepted the WhatsApp change, so the API returns an error while the local contact remains stale; a profile-picture lookup failure also prevents the webhook from being sent.

Make local synchronization failure-safe after the remote patch, retrying or reconciling failed contact updates instead of leaving the accepted change stale.

Prompt for AI agents
In `src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts` at line 4602:

**Accepted updates leave contacts stale**

When baileys accepts the patch, then the profile-picture lookup, event adapter, or PN contact write in `contacts.update` fails, `updateContact` propagates the `contacts.update` failure after `addOrEditContact` has accepted the WhatsApp change, so the API returns an error while the local contact remains stale; a profile-picture lookup failure also prevents the webhook from being sent.

Make local synchronization failure-safe after the remote patch, retrying or reconciling failed contact updates instead of leaving the accepted change stale.

type: 'object',
properties: {
number: { type: 'string' },
fullName: { type: 'string' },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Long names fail local persistence

When fullName exceeds 100 characters and contact persistence is enabled, the schema accepts the name, but the Contact pushName column is limited to 100 characters. Baileys accepts the patch before Prisma rejects the local write, so the request fails and the local contact remains stale.

Set a 100-character maximum for fullName in updateContactSchema.

Prompt for AI agents
In `src/validate/chat.schema.ts` at line 327:

**Long names fail local persistence**

When `fullName` exceeds 100 characters and contact persistence is enabled, the schema accepts the name, but the Contact `pushName` column is limited to 100 characters. Baileys accepts the patch before Prisma rejects the local write, so the request fails and the local contact remains stale.

Set a 100-character maximum for `fullName` in `updateContactSchema`.

}

// emitOwnEvents is off, so Baileys will not echo this change back to us.
await this.contactHandle['contacts.update']([{ id: pnJid, name: data.fullName }]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Saved avatars disappear

When a contact has a saved avatar and the profile-picture lookup during a name update fails or returns null, contacts.update writes the null profilePicture result into profilePicUrl, replacing the saved avatar, so contact listings lose it.

Preserve the existing profilePicUrl when the lookup fails or returns null.

Prompt for AI agents
In `src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts` at line 4602:

**Saved avatars disappear**

When a contact has a saved avatar and the profile-picture lookup during a name update fails or returns null, `contacts.update` writes the null `profilePicture` result into `profilePicUrl`, replacing the saved avatar, so contact listings lose it.

Preserve the existing `profilePicUrl` when the lookup fails or returns null.

saveOnPrimaryAddressbook: { type: 'boolean' },
},
required: ['number', 'fullName'],
...isNotEmpty('number', 'fullName'),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Empty names are accepted

When the request includes an unrecognized property or an optional field and fullName is empty, isNotEmpty skips the minLength check when a property name is outside its enum, so the empty fullName reaches addOrEditContact and leaves the contact with a blank name.

Validate that fullName is nonempty independently of the other request properties.

Prompt for AI agents
In `src/validate/chat.schema.ts` at line 332:

**Empty names are accepted**

When the request includes an unrecognized property or an optional field and `fullName` is empty, `isNotEmpty` skips the `minLength` check when a property name is outside its enum, so the empty `fullName` reaches `addOrEditContact` and leaves the contact with a blank name.

Validate that `fullName` is nonempty independently of the other request properties.

Comment on lines +4602 to +4610

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · PN and LID names diverge

When the PN update succeeds but the separate LID contact update fails, the PN row is committed while the LID row retains the old name, so LID-based contact reads remain stale and the caller receives an error despite the accepted WhatsApp patch.

Make the PN and LID contact updates atomic, or retry/reconcile a failed LID update.

Prompt for AI agents
In `src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts` at lines 4602-4610:

**PN and LID names diverge**

When the PN update succeeds but the separate LID contact update fails, the PN row is committed while the LID row retains the old name, so LID-based contact reads remain stale and the caller receives an error despite the accepted WhatsApp patch.

Make the PN and LID contact updates atomic, or retry/reconcile a failed LID update.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant