Skip to content

Commit 8168f90

Browse files
authored
feat: send true WhatsApp voice notes (PTT) via send-audio, bulk, and MCP (#559)
Adds an opt-in `ptt` boolean so send-audio, bulk send, and the MessageSendAudio MCP tool can send a true WhatsApp voice note (mic bubble + waveform) on both engines. Backward compatible (default off); outbound voice notes persist as type 'voice'; server defaults the mimetype to audio/ogg; codecs=opus when ptt is set without one; no transcoding. SDK types + docs updated. Fulfils FR-MSG-004. (OpenWA-n8n #13)
1 parent eae546e commit 8168f90

21 files changed

Lines changed: 197 additions & 21 deletions

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- **Send true WhatsApp voice notes (PTT).** The `send-audio` endpoint, bulk send, and the `MessageSendAudio` agent tool now accept an optional `ptt` boolean; when set, the message is delivered as a real voice note — the microphone bubble with a waveform — instead of a plain audio file, on both the Baileys and whatsapp-web.js engines. Voice notes require `audio/ogg; codecs=opus` audio, so the server defaults the mimetype to that when `ptt` is set without one (supply OGG/Opus bytes for reliable playback), and stores the message as `type: "voice"`. Fulfills FR-MSG-004. (OpenWA-n8n #13)
13+
1014
### Fixed
1115

1216
- **Sending to — or operating on — a WhatsApp Channel (newsletter) no longer logs internal errors.** On the whatsapp-web.js engine a channel JID (`…@newsletter`) resolves to a `Channel`, which has none of the per-chat operations; the gateway now skips those for channels instead of throwing. The typing indicator that precedes a send, the typing/recording presence endpoint and its MCP tool, mark-unread, and delete-chat now cleanly no-op for a channel (presence does nothing; mark-unread and delete-chat report no change) rather than emitting an internal `TypeError`. Fetching chat labels for a channel previously failed with HTTP 500 — it now returns an empty list. Direct chats, groups, and broadcast lists are unaffected. (#554) Thanks @DanielOberlechner.

docs/06-api-specification.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -996,7 +996,7 @@ Send a video (by URL or base64) with an optional caption. Uses the same `SendMed
996996

997997
#### POST /api/sessions/:sessionId/messages/send-audio
998998

999-
Send an audio/voice message (by URL or base64). Uses `SendMediaMessageDto`. A `caption` is accepted by the DTO but not persisted for audio.
999+
Send an audio message (by URL or base64). Uses `SendAudioMessageDto`. A `caption` is accepted by the DTO but not persisted for audio. Set `ptt: true` to send a real WhatsApp **voice note** (microphone bubble + waveform) instead of a plain audio file. `ptt` is a JSON boolean, exclusive to this endpoint, and — because voice notes require `audio/ogg; codecs=opus` — the server defaults the mimetype to that when you set `ptt` without one; for reliable playback (especially on the Baileys engine, which does not transcode) supply OGG/Opus bytes. A `ptt` voice note is stored as message `type: "voice"`.
10001000

10011001
**Auth:** API key (OPERATOR)
10021002

@@ -1006,10 +1006,10 @@ Send an audio/voice message (by URL or base64). Uses `SendMediaMessageDto`. A `c
10061006
| --- | --- | --- |
10071007
| sessionId | string | Session ID |
10081008

1009-
**Request body**`SendMediaMessageDto` (fields `chatId`, `url`, `base64`, `mimetype`, `filename`, `caption`see `send-image`)
1009+
**Request body**`SendAudioMessageDto` (all `SendMediaMessageDto` fields `chatId`, `url`, `base64`, `mimetype`, `filename`, `caption`plus optional `ptt` boolean)
10101010

10111011
```json
1012-
{ "chatId": "628123456789@c.us", "url": "https://example.com/voice.ogg", "mimetype": "audio/ogg" }
1012+
{ "chatId": "628123456789@c.us", "url": "https://example.com/voice.ogg", "mimetype": "audio/ogg", "ptt": true }
10131013
```
10141014

10151015
**Response** `201`

docs/07-api-collection.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -278,13 +278,13 @@ curl -X POST "$BASE/api/sessions/my-session/messages/send-video" \
278278

279279
#### POST /api/sessions/:sessionId/messages/send-audio
280280

281-
Send an audio/voice message by URL or base64.
281+
Send an audio message by URL or base64. Add `"ptt": true` to send a real WhatsApp voice note (mic bubble + waveform); the server defaults the mimetype to `audio/ogg; codecs=opus` when `ptt` is set without one.
282282

283283
```bash
284284
curl -X POST "$BASE/api/sessions/my-session/messages/send-audio" \
285285
-H "X-API-Key: $API_KEY" \
286286
-H "Content-Type: application/json" \
287-
-d '{ "chatId": "628123456789@c.us", "url": "https://example.com/voice.ogg", "mimetype": "audio/ogg" }'
287+
-d '{ "chatId": "628123456789@c.us", "url": "https://example.com/voice.ogg", "mimetype": "audio/ogg", "ptt": true }'
288288
```
289289

290290
#### POST /api/sessions/:sessionId/messages/send-document

sdk/javascript/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ console.log(result.messageId);
3333

3434
CommonJS consumers use `require('@rmyndharis/openwa')` identically.
3535

36+
## Messaging
37+
38+
> Voice notes: pass `ptt: true` to `sendAudio` to send a real WhatsApp voice note (PTT). Supply `audio/ogg; codecs=opus` audio for reliable playback; the server defaults the mimetype to that when `ptt` is set without one.
39+
3640
## Errors
3741

3842
Non-2xx responses throw a typed `OpenWAApiError` subclass

sdk/javascript/src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ export interface SendMediaRequest {
108108
filename?: string;
109109
/** Max 1024 chars. */
110110
caption?: string;
111+
/** Audio only: send as a WhatsApp voice note (PTT). Server defaults mimetype to audio/ogg; codecs=opus. */
112+
ptt?: boolean;
111113
}
112114

113115
export interface SendLocationRequest {

sdk/php/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ $client = new Client([
4444
]);
4545
```
4646

47+
## Messaging
48+
49+
> Voice notes: pass `'ptt' => true` to `sendAudio` to send a real WhatsApp voice note (PTT). Supply `audio/ogg; codecs=opus` audio for reliable playback; the server defaults the mimetype to that when `ptt` is set without one.
50+
4751
## Errors
4852

4953
A non-2xx response throws a typed `OpenWA\Exceptions\OpenWAApiException` subclass —

sdk/python/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ import httpx
4545
client = OpenWAClient(base_url="", api_key="", transport=httpx.MockTransport(handler))
4646
```
4747

48+
## Messaging
49+
50+
> Voice notes: pass `ptt=True` inside the body dict to `send_audio` to send a real WhatsApp voice note (PTT). Supply `audio/ogg; codecs=opus` audio for reliable playback; the server defaults the mimetype to that when `ptt` is set without one.
51+
4852
## Errors
4953

5054
A non-2xx response raises a typed `OpenWAApiError` subclass — `OpenWAAuthError` (401),

sdk/python/openwa/types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ class SendMediaRequest(TypedDict, total=False):
107107
mimetype: str
108108
filename: str
109109
caption: str
110+
ptt: bool # audio only: send as a WhatsApp voice note (PTT)
110111

111112

112113
class SendLocationRequest(TypedDict, total=False):

src/core/agent-tools/tools/message.tools.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ export function messageTools(message: MessageService): ToolDescriptor[] {
157157
mimetype: z.string().optional().describe('MIME type (required when using base64)'),
158158
filename: z.string().max(255).optional(),
159159
caption: z.string().max(1024).optional(),
160+
ptt: z.boolean().optional().describe('Send as a WhatsApp voice note (PTT)'),
160161
}),
161162
handler: (input: {
162163
sessionId: string;
@@ -166,6 +167,7 @@ export function messageTools(message: MessageService): ToolDescriptor[] {
166167
mimetype?: string;
167168
filename?: string;
168169
caption?: string;
170+
ptt?: boolean;
169171
}) =>
170172
message.sendAudio(input.sessionId, {
171173
chatId: input.chatId,
@@ -174,6 +176,7 @@ export function messageTools(message: MessageService): ToolDescriptor[] {
174176
mimetype: input.mimetype,
175177
filename: input.filename,
176178
caption: input.caption,
179+
ptt: input.ptt,
177180
}),
178181
},
179182
{

src/engine/adapters/baileys.adapter.spec.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1409,6 +1409,20 @@ describe('BaileysAdapter media sends', () => {
14091409
});
14101410
});
14111411

1412+
it('sendAudioMessage with ptt sends a voice note (ptt:true)', async () => {
1413+
const adapter = await ready();
1414+
await adapter.sendAudioMessage('628111@s.whatsapp.net', {
1415+
mimetype: 'audio/ogg; codecs=opus',
1416+
data: Buffer.from([1]),
1417+
ptt: true,
1418+
});
1419+
expect(fakeSock.sendMessage).toHaveBeenCalledWith('628111@s.whatsapp.net', {
1420+
audio: Buffer.from([1]),
1421+
mimetype: 'audio/ogg; codecs=opus',
1422+
ptt: true,
1423+
});
1424+
});
1425+
14121426
it('sendStickerMessage sends the sticker buffer', async () => {
14131427
const adapter = await ready();
14141428
await adapter.sendStickerMessage('628111@s.whatsapp.net', { mimetype: 'image/webp', data: Buffer.from([7]) });

0 commit comments

Comments
 (0)