Overview
Call Recordings lets you store phone call audio with summaries, transcripts, and contact links. There are two ways to add recordings:
Upload from PRM
RecommendedUpload audio files directly from the dashboard. Large files upload straight to S3 from your browser — no size limits through the PRM server.
Webhook automation
Optional. Send AI summaries from AWS Lambda or your phone system using a per-user webhook secret.
Upload from PRM
The primary way to add recordings is from inside PRM — no webhook or external setup required.
- Go to Call Recordings in the sidebar (
/dashboard/call-recordings). - Click Upload recording.
- Choose an audio file (MP3, WAV, M4A, WebM, OGG — up to 500 MB). PRM requests a presigned S3 URL, then your browser uploads the file directly to storage.
- Add optional details: title, date & time, call number (your SIM / line), receiver number (other party), contact, summary, and transcript.
- Click Upload. PRM saves the metadata and opens the recording detail page with audio playback.
Why this works for large files: The audio never passes through PRM's server. Only a small presigned URL request and a metadata save happen on PRM — the heavy file upload goes browser → S3 directly.
Manage recordings
Sort & search
Sort by call date, date added, title, call number, or receiver number. Search across titles, summaries, transcripts, phone numbers, and contact names. Filters sync to the URL so you can bookmark or share views.
Archive & restore
Archive recordings from the list menu or detail page. View archived items at /dashboard/call-recordings/archived. Restore anytime.
Assign contacts
Link a recording to any contact from the detail page. Assigned recordings also appear on the contact's profile.
Playback
Play audio inline on the detail page and from contact profiles. PRM serves audio via secure presigned S3 URLs.
How PRM stores audio
Whether you upload from the dashboard or via webhook automation, audio is stored in your configured S3 bucket under:
recordings/{your-user-id}/{optional-contact-id}/{id}-{filename}PRM requires AWS S3 credentials on the server (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, AWS_S3_BUCKET). Your S3 bucket CORS configuration must allow PUT from your PRM domain for browser uploads to work.
Optional — external automation
The sections below are only needed if you want an external service (e.g. AWS Lambda call-summary) to push recordings into PRM automatically. If you upload manually from PRM, you can skip them.
Webhook integration
External automations POST call metadata to PRM after processing. Generate a per-user secret in Settings → Call Recordings Webhook.
POST https://www.ninjaprm.com.au/api/webhooks/call-recordingsWebhook setup
- Go to Settings → Call Recordings Webhook and click Generate secret.
- Copy the webhook URL, upload URL endpoint, and secret.
- Configure your automation to use the three-step flow below. Never send audio as base64 in the webhook JSON.
Webhook large files
Serverless hosts reject webhook bodies over ~4.5MB. Sending audioBase64 returns 413 Request Entity Too Large. Use the same pattern as PRM dashboard uploads: presigned S3 URL first, metadata webhook second.
POST https://www.ninjaprm.com.au/api/webhooks/call-recordings/upload-url// Automation only — do not send audio in the webhook JSON body
// 1. Get presigned upload URL
const uploadRes = await fetch("https://www.ninjaprm.com.au/api/webhooks/call-recordings/upload-url", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_WEBHOOK_SECRET",
"Content-Type": "application/json",
},
body: JSON.stringify({
externalId: call.id,
filename: "recording.mp3",
mimeType: "audio/mpeg",
}),
});
const { uploadUrl, s3Key, mimeType } = await uploadRes.json();
// 2. Upload audio directly to S3
await fetch(uploadUrl, {
method: "PUT",
headers: { "Content-Type": mimeType },
body: audioBuffer,
});
// 3. Send metadata only
await fetch("https://www.ninjaprm.com.au/api/webhooks/call-recordings", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_WEBHOOK_SECRET",
"Content-Type": "application/json",
},
body: JSON.stringify({
externalId: call.id,
s3Key,
callNumber: call.callNumber,
receiverNumber: call.receiverNumber,
title: summary.title,
summary: summary.text,
transcript: call.transcript,
}),
});Webhook auth
Authorization: Bearer YOUR_WEBHOOK_SECRETEach user has their own secret. With a valid bearer token, userId in the payload is optional.
Webhook payload
Metadata only — reference audio via s3Key, sourceS3Key, or audioUrl.
Phone numbers
Store both sides of the call separately — important when you use multiple SIM cards or lines:
callNumber— your line / SIM (aliases:call,from,fromNumber)receiverNumber— the other party (aliases:receiver,to,toNumber)
Legacy caller / callerNumber fields map to callNumber.
{
"externalId": "call-2026-06-18-045203",
"callNumber": "+61400111222",
"receiverNumber": "+61400999888",
"occurredAt": "2026-06-18T04:52:21.000Z",
"title": "Brief Disconnected Call Attempt",
"sentiment": "Frustrated",
"summary": "The caller experienced difficulty with the call connection...",
"transcript": "Hello? Hello? ...",
"s3Key": "recordings/your-user-id/call-id-recording.mp3"
}Contact matching
PRM links recordings to contacts automatically when possible. This applies to webhook ingest and PRM uploads.
- Explicit
contactIdin the webhook payload or selected during PRM upload. - Otherwise, match
callNumberorreceiverNumberagainst contact phone numbers (flexible formatting — spaces, country codes, etc.). If both numbers match different contacts, the receiver number wins. - Otherwise unassigned — assign manually from the detail page.
Error codes
| Status | Meaning |
|---|---|
| 401 | Missing or invalid webhook bearer token |
| 400 | Invalid payload or missing audio reference |
| 404 | User not found (webhook) |
| 413 | Webhook body too large — use presigned S3 upload, not base64 |
| 500 | S3 or server error |