# Recording calls

This guide covers capturing audio from a call with the `<Record>` verb, receiving the result on your server, and fetching recordings after the fact over the REST API.

## Record a voicemail message

The most common recording pattern prompts the caller, then records a message with `<Record>`. When the caller stops speaking, presses `#`, or the recording reaches its maximum length, PressOne requests the `action` URL with the recording result.

<CodeTabs syncKey="lang">

```javascript title="Node.js"
const express = require("express");
const app = express();
app.use(express.urlencoded({ extended: false }));

app.post("/voice/incoming", (req, res) => {
  const oneml = `<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Say>Please leave a message after the beep. Press pound when finished.</Say>
  <Record action="/voice/voicemail-complete" maxLength="120" finishOnKey="#"/>
  <Say>We did not receive a recording. Goodbye.</Say>
  <Hangup/>
</Response>`;

  res.type("text/xml").send(oneml);
});

app.post("/voice/voicemail-complete", (req, res) => {
  console.log("Voicemail received:", {
    recordingSid: req.body.RecordingSid,
    recordingUrl: req.body.RecordingUrl,
    durationSeconds: req.body.RecordingDuration,
  });

  const oneml = `<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Say>Thank you. Your message has been recorded. Goodbye.</Say>
  <Hangup/>
</Response>`;

  res.type("text/xml").send(oneml);
});

app.listen(3000);
```

```python title="Python"
from flask import Flask, request, Response

app = Flask(__name__)

@app.route("/voice/incoming", methods=["POST"])
def voice_incoming():
    oneml = """<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Say>Please leave a message after the beep. Press pound when finished.</Say>
  <Record action="/voice/voicemail-complete" maxLength="120" finishOnKey="#"/>
  <Say>We did not receive a recording. Goodbye.</Say>
  <Hangup/>
</Response>"""

    return Response(oneml, mimetype="text/xml")

@app.route("/voice/voicemail-complete", methods=["POST"])
def voicemail_complete():
    print("Voicemail received:", {
        "recordingSid": request.form.get("RecordingSid"),
        "recordingUrl": request.form.get("RecordingUrl"),
        "durationSeconds": request.form.get("RecordingDuration"),
    })

    oneml = """<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Say>Thank you. Your message has been recorded. Goodbye.</Say>
  <Hangup/>
</Response>"""

    return Response(oneml, mimetype="text/xml")

if __name__ == "__main__":
    app.run(port=3000)
```

</CodeTabs>

`RecordingSid`, `RecordingUrl`, and `RecordingDuration` are the parameters PressOne posts to `action`. See [Record](/voice/oneml/record) for the complete attribute reference, including `timeout`, `finishOnKey`, and `maxLength`.

## Get notified when a recording finishes

Some recordings are not tied to a single `action` (for example, logging an entire call for quality purposes rather than collecting a message). Set `recordingStatusCallback` instead of (or alongside) `action` to have PressOne notify a separate URL once the recording finishes:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Say>This call may be recorded for quality purposes.</Say>
  <Record playBeep="false" timeout="0" maxLength="3600" recordingStatusCallback="/voice/recording-status"/>
  <Hangup/>
</Response>
```

PressOne requests `recordingStatusCallback` once, for the `completed` event only: there is no separate `in-progress` notification. The request carries these parameters:

| Parameter | Description |
|-----------|-------------|
| `AccountSid` | Your account SID. |
| `CallSid` | The unique identifier of the call. |
| `RecordingSid` | Unique identifier for the recording. |
| `RecordingUrl` | Public URL of the recorded audio file. |
| `RecordingStatus` | Always `completed`. |
| `RecordingDuration` | Duration of the recording, in seconds. |
| `RecordingChannels` | Always `1`. |
| `RecordingSource` | Always `RecordVerb`. |

The receiver only needs to acknowledge the request. It does not control the call, since `<Record>` has already released it to the next verb:

<CodeTabs syncKey="lang">

```javascript title="Node.js"
const express = require("express");
const app = express();
app.use(express.urlencoded({ extended: false }));

app.post("/voice/recording-status", (req, res) => {
  console.log("Recording completed:", {
    recordingSid: req.body.RecordingSid,
    recordingUrl: req.body.RecordingUrl,
    durationSeconds: req.body.RecordingDuration,
  });
  res.sendStatus(200);
});

app.listen(3000);
```

```python title="Python"
from flask import Flask, request

app = Flask(__name__)

@app.route("/voice/recording-status", methods=["POST"])
def recording_status():
    print("Recording completed:", {
        "recordingSid": request.form.get("RecordingSid"),
        "recordingUrl": request.form.get("RecordingUrl"),
        "durationSeconds": request.form.get("RecordingDuration"),
    })
    return "", 200

if __name__ == "__main__":
    app.run(port=3000)
```

</CodeTabs>

## Recording URLs are public

`RecordingUrl`, delivered on both `action` and `recordingStatusCallback`, is publicly readable: anyone who has the URL can play or download the recording, with no authentication required. Treat it as sensitive data:

- Store it server-side rather than passing it directly to a browser or mobile client.
- Avoid writing it to logs your support team or third-party tools can read.
- If a caller-facing product needs to play a recording back, proxy the audio through an endpoint your own server authenticates, instead of forwarding the raw `RecordingUrl`.

## Fetch recordings over REST

Once you have a `CallSid`, you can also retrieve recordings after the fact instead of relying on `action` or `recordingStatusCallback` alone.

### List every recording for a call

<CodeTabs syncKey="lang">

```bash title="curl"
curl -u ACCOUNT_SID:AUTH_TOKEN \
  "https://api.helios.pressone.co/2010-04-01/Accounts/ACCOUNT_SID/Calls/CA5388f7a5e04d0ed71cac5cdf1ea17a53/Recordings.json"
```

```javascript title="Node.js"
const accountSid = "ACCOUNT_SID";
const authToken = "AUTH_TOKEN";
const callSid = "CA5388f7a5e04d0ed71cac5cdf1ea17a53";

const response = await fetch(
  `https://api.helios.pressone.co/2010-04-01/Accounts/${accountSid}/Calls/${callSid}/Recordings.json`,
  {
    headers: {
      Authorization: `Basic ${Buffer.from(`${accountSid}:${authToken}`).toString("base64")}`,
    },
  },
);

const recordings = await response.json();
console.log(recordings.recordings.map((recording) => recording.sid));
```

```python title="Python"
import requests

account_sid = "ACCOUNT_SID"
auth_token = "AUTH_TOKEN"
call_sid = "CA5388f7a5e04d0ed71cac5cdf1ea17a53"

response = requests.get(
    f"https://api.helios.pressone.co/2010-04-01/Accounts/{account_sid}/Calls/{call_sid}/Recordings.json",
    auth=(account_sid, auth_token),
)

recordings = response.json()
print([recording["sid"] for recording in recordings["recordings"]])
```

</CodeTabs>

<OpenPlaygroundButton
  server="https://api.helios.pressone.co"
  url="/2010-04-01/Accounts/{AccountSid}/Calls/{CallSid}/Recordings.json"
  method="GET"
  pathParams={[
    { name: "AccountSid", defaultValue: "ACCOUNT_SID" },
    { name: "CallSid", defaultValue: "CA5388f7a5e04d0ed71cac5cdf1ea17a53" },
  ]}
>
  Try it in the playground
</OpenPlaygroundButton>

This list is not paginated, and each entry is metadata only: it does not include `RecordingUrl`. Capture that value from `action` or `recordingStatusCallback` when you need to play or download the audio itself:

```json
{
  "recordings": [
    {
      "sid": "REd947fbd985d13ecb4c31eca3186f20bb",
      "account_sid": "AC9ce1cd445308e3ec5080c6126288aa09",
      "call_sid": "CA5388f7a5e04d0ed71cac5cdf1ea17a53",
      "duration": 42,
      "format": "wav",
      "date_created": "2026-07-02T12:00:00Z",
      "date_updated": "2026-07-02T12:00:42Z",
      "uri": "/2010-04-01/Accounts/AC9ce1cd445308e3ec5080c6126288aa09/Recordings/REd947fbd985d13ecb4c31eca3186f20bb.json"
    }
  ],
  "uri": "/2010-04-01/Accounts/AC9ce1cd445308e3ec5080c6126288aa09/Calls/CA5388f7a5e04d0ed71cac5cdf1ea17a53/Recordings.json"
}
```

### Fetch a single recording

<CodeTabs syncKey="lang">

```bash title="curl"
curl -u ACCOUNT_SID:AUTH_TOKEN \
  "https://api.helios.pressone.co/2010-04-01/Accounts/ACCOUNT_SID/Recordings/REd947fbd985d13ecb4c31eca3186f20bb.json"
```

```javascript title="Node.js"
const accountSid = "ACCOUNT_SID";
const authToken = "AUTH_TOKEN";
const recordingSid = "REd947fbd985d13ecb4c31eca3186f20bb";

const response = await fetch(
  `https://api.helios.pressone.co/2010-04-01/Accounts/${accountSid}/Recordings/${recordingSid}.json`,
  {
    headers: {
      Authorization: `Basic ${Buffer.from(`${accountSid}:${authToken}`).toString("base64")}`,
    },
  },
);

const recording = await response.json();
console.log(recording.sid, recording.duration);
```

```python title="Python"
import requests

account_sid = "ACCOUNT_SID"
auth_token = "AUTH_TOKEN"
recording_sid = "REd947fbd985d13ecb4c31eca3186f20bb"

response = requests.get(
    f"https://api.helios.pressone.co/2010-04-01/Accounts/{account_sid}/Recordings/{recording_sid}.json",
    auth=(account_sid, auth_token),
)

recording = response.json()
print(recording["sid"], recording["duration"])
```

</CodeTabs>

<OpenPlaygroundButton
  server="https://api.helios.pressone.co"
  url="/2010-04-01/Accounts/{AccountSid}/Recordings/{RecordingSid}.json"
  method="GET"
  pathParams={[
    { name: "AccountSid", defaultValue: "ACCOUNT_SID" },
    { name: "RecordingSid", defaultValue: "REd947fbd985d13ecb4c31eca3186f20bb" },
  ]}
>
  Try it in the playground
</OpenPlaygroundButton>

A completed recording returns its `duration` and `format`:

```json
{
  "sid": "REd947fbd985d13ecb4c31eca3186f20bb",
  "account_sid": "AC9ce1cd445308e3ec5080c6126288aa09",
  "call_sid": "CA5388f7a5e04d0ed71cac5cdf1ea17a53",
  "duration": 42,
  "format": "wav",
  "date_created": "2026-07-02T12:00:00Z",
  "date_updated": "2026-07-02T12:00:42Z",
  "uri": "/2010-04-01/Accounts/AC9ce1cd445308e3ec5080c6126288aa09/Recordings/REd947fbd985d13ecb4c31eca3186f20bb.json"
}
```

If you fetch a recording before it finishes, `duration` and `format` are not yet known. The API omits both fields from the response entirely rather than returning them as `null`:

```json
{
  "sid": "REd947fbd985d13ecb4c31eca3186f20bb",
  "account_sid": "AC9ce1cd445308e3ec5080c6126288aa09",
  "call_sid": "CA5388f7a5e04d0ed71cac5cdf1ea17a53",
  "date_created": "2026-07-02T12:00:00Z",
  "date_updated": "2026-07-02T12:00:00Z",
  "uri": "/2010-04-01/Accounts/AC9ce1cd445308e3ec5080c6126288aa09/Recordings/REd947fbd985d13ecb4c31eca3186f20bb.json"
}
```

A missing `RecordingSid` returns `404` instead of a partial resource.

## Next steps

- [Record](/voice/oneml/record): the complete `<Record>` attribute reference.
- [Forwarding calls](/voice/forwarding-calls): combine `<Dial>` with a voicemail fallback when the dialed party does not answer.
- [Webhooks](/voice/webhooks): standard parameters and signature verification for every request PressOne sends your server.
- [API reference](/api/voice): complete REST endpoint documentation.
