Branch:
fix/media-key-jsonb-updateMediaMessage4 commits on top ofrelease/2.3.7— all production-tested.
This patch adds 3 new endpoints + 2 bug fixes to Evolution API, making it self-sufficient for WhatsApp media recovery without external orchestrators.
WhatsApp CDN URLs expire after ~7 days. Once expired, media files (SOR documents, images, etc.) become permanently inaccessible unless you:
- Have the original
mediaKey+directPath(stored in EA's Message table) - Can trigger WhatsApp's
updateMediaMessageprotocol (asks sender to re-upload) - Can request historical messages via
fetchMessageHistory(on-demand history sync)
Previously, only OwnPilot had these capabilities. Now EA has them natively.
Purpose: Download media using caller-supplied metadata — does NOT require message to exist in EA's DB.
When to use:
- You have
mediaKey+directPathfrom an external source (e.g., OwnPilot DB) - The message exists in EA but
getBase64FromMediaMessagefails (DB lookup issue)
Request:
{
"messageId": "3EB0D228037ED522E72774",
"remoteJid": "120363423491841999@g.us",
"participant": "119365882089638@lid",
"fromMe": false,
"mediaKey": "base64-encoded-key",
"directPath": "/v/t62.7119-24/...",
"url": "https://mmg.whatsapp.net/...",
"mimeType": "application/octet-stream",
"filename": "2314CP_82_V1.SOR",
"fileLength": 20973,
"convertToMp4": false
}Response:
{
"base64": "TWFwAMgAfA...",
"mimetype": "application/octet-stream",
"filename": "2314CP_82_V1.SOR"
}Algorithm:
- Reconstruct minimal WAMessage proto from provided metadata
- Try direct
downloadMediaMessage(fast-path — CDN still valid) - On failure → explicit
updateMediaMessagewith 30s timeout (Baileys RC9 workaround) - Retry download with refreshed URL
Edge Cases:
mediaKeyfrom PostgreSQL JSONB may be stored as{0: 123, 1: 45, ...}object instead of Uint8Array — the code handles both formats via lexicographic sort fix (commit262c9300)updateMediaMessagetimes out after 30s if sender is permanently offline — throwsBadRequestException- Audio files with
convertToMp4: trueare processed viaprocessAudioMp4
Purpose: Trigger WhatsApp on-demand history sync for a group. WhatsApp responds with old message protos containing fresh mediaKey + directPath.
When to use:
- Messages are missing from EA's DB (were sent before EA was connected)
- You need fresh mediaKeys for messages whose CDN URLs expired
Request:
{
"groupJid": "120363423491841999@g.us",
"count": 50,
"anchorMessageId": "3EB0DCCA32F22B9AA2A3B4",
"anchorTimestamp": 1765216930,
"anchorFromMe": false,
"anchorParticipant": "90383560261829@lid"
}Response (immediate — 202-style):
{
"sessionId": "3EB006B411C1B0933F9410",
"groupJid": "120363423491841999@g.us",
"count": 50,
"message": "History sync requested. WhatsApp will deliver messages via messaging-history.set event (async)."
}Algorithm:
- Validate groupJid ends with
@g.us - Rate-limit check (1 call per 30 seconds)
- Call
sock.fetchMessageHistory(count, anchorKey, anchorTimestamp) - WhatsApp delivers messages asynchronously via
messaging-history.setevent - Messages are stored in DB if
DATABASE_SAVE_DATA_HISTORIC=true
CRITICAL Prerequisites:
DATABASE_SAVE_DATA_HISTORIC=truemust be set in env — otherwise messages arrive but are NOT saved to DBdaysLimitImportMessagesin Chatwoot config should be high (e.g., 1000) — otherwise old messages are filtered out- EA must be the sole linked device on the WhatsApp number — if another client (e.g., OwnPilot) is connected, WhatsApp may route the response to that client instead
Edge Cases:
- Rate limited: 1 call per 30 seconds. Calling faster throws
BadRequestExceptionwith wait time - Empty anchor (
anchorMessageId: "") — WhatsApp may not respond at all - WhatsApp returns messages OLDER than the anchor (backward direction only)
- Duplicate messages are handled by
messagesRepository.has(m.key.id)check — no duplicates in DB - Max 50 messages per call (WhatsApp protocol limit)
- Response is async — poll DB count or check logs to verify delivery
Iterative Fetching Pattern:
1. Find oldest message in DB → use as anchor
2. Call fetchGroupHistory
3. Wait 35s (30s rate-limit + 5s buffer)
4. Check if DB count increased
5. If increased → repeat from step 1 (new oldest message = new anchor)
6. If no increase → reached beginning of history
Purpose: End-to-end batch recovery pipeline. For each message: DB lookup → download → MinIO upload → media record → mediaUrl update.
When to use:
- You have message IDs in EA's DB with expired CDN URLs
- You want to permanently store media in MinIO (S3) and update DB references
Request:
{
"messageIds": ["3EB0D228037ED522E72774", "3EB0DCCA32F22B9AA2A3B4"],
"continueOnError": true,
"storeToMinIO": true
}Response:
{
"total": 2,
"ok": 1,
"skip": 1,
"error": 0,
"results": [
{
"messageId": "3EB0D228037ED522E72774",
"status": "ok",
"mediaUrl": "http://minio:9000/evolution-media/..."
},
{
"messageId": "3EB0DCCA32F22B9AA2A3B4",
"status": "skip",
"error": "Already stored in MinIO"
}
]
}Algorithm per message:
- Fetch message from DB by
key.id+instanceId - Extract media metadata from
documentMessage | imageMessage | videoMessage | audioMessage | stickerMessage - Skip if no
mediaKey/directPath - Skip if
mediaUrlalready points to non-WhatsApp URL (already in MinIO) - Handle JSONB mediaKey format:
Object.keys().sort((a,b)=>parseInt(a)-parseInt(b))for numeric key ordering - Call
retryMediaFromMetadatawithgetBuffer=true - Upload buffer to MinIO via
s3Service.uploadFile - Upsert
Mediarecord in DB - Update
message.mediaUrlin the document message content
Edge Cases:
- JSONB mediaKey sort: PostgreSQL stores
{0:x, 1:y, 10:z, 2:w}— lexicographic sort gives wrong byte order. Numeric sort fix applied. continueOnError: false— stops at first failure, returns partial resultsstoreToMinIO: false— downloads but doesn't upload (useful for testing)- S3 not enabled — downloads and reports size but doesn't upload
- Message not found in DB →
status: "skip" - Empty buffer after download →
status: "error" - Presigned URLs in
mediaUrlexpire after 7 days — but the object persists in MinIO. Generate new presigned URL vias3Service.getObjectUrl()
Batch Processing Pattern:
# Recommended: 10 per batch, 1-2s delay between batches
for batch in chunks(message_ids, 10):
response = POST /chat/batchRecoverMedia/{instance} { messageIds: batch }
# Each batch takes ~10-30s depending on CDN/updateMediaMessageProblem: PostgreSQL stores Uint8Array as JSONB object {0: 182, 1: 45, 10: 67, 2: 99, ...}. JavaScript Object.keys() returns lexicographic order: ["0", "1", "10", "2", ...] — wrong byte sequence → HKDF decryption fails.
Fix: Object.keys(mediaKey).sort((a, b) => parseInt(a) - parseInt(b)).map(k => mediaKey[k])
Affected: getBase64FromMediaMessage + batchRecoverMedia
Problem: Baileys 7.0.0-rc.9 wires reuploadRequest callback in download options, but the catch block checks error.status while the actual error has output.statusCode — callback never triggers on 410/404.
Fix: Explicit updateMediaMessage() call in the catch block with 30s timeout, bypassing Baileys' broken internal retry.
Affected: getBase64FromMediaMessage + retryMediaFromMetadata
Required for history sync to work:
DATABASE_SAVE_DATA_HISTORIC=true # MUST be set — otherwise messaging-history.set messages are droppedRequired for old message import:
-- In Chatwoot table, increase daysLimitImportMessages (default: 3 days)
UPDATE "Chatwoot" SET "daysLimitImportMessages" = 1000
WHERE "instanceId" = '<your-instance-id>';Required for MinIO storage:
S3_ENABLED=true
S3_BUCKET=evolution-media
S3_PORT=9000
S3_ENDPOINT=minio
S3_ACCESS_KEY=<key>
S3_SECRET_KEY=<secret>Tested on GoConnectIT WhatsApp instance (Euronet SOR documents):
| Metric | Before | After |
|---|---|---|
| Total messages | 1646 | 1870 (+224) |
| Oldest message | Dec 8, 2025 | Nov 10, 2025 |
| SOR files in MinIO | 0 | 1132/1137 (99.6%) |
| Irrecoverable | — | 5 (sender permanently offline) |
| File | Changes |
|---|---|
src/api/dto/chat.dto.ts |
+3 DTOs: RetryMediaFromMetadataDto, FetchGroupHistoryDto, BatchRecoverMediaDto |
src/api/controllers/chat.controller.ts |
+3 controller methods |
src/api/routes/chat.router.ts |
+3 route registrations |
src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts |
+3 service methods, 2 bug fixes |