# Faxes

Send faxes from your fax lines and read your fax history.

## Send a fax

`POST /v1/faxes`

Sends one fax from one of your fax lines. Send the document as a base64-encoded PDF of at most 25 pages, up to 20 MB before encoding. Another file type, a longer PDF, or a PDF whose pages cannot be read is refused with `400`.

**Limits.** Per account: 3 requests a minute and 50 faxes sent a day (Eastern calendar day). The fax API is for sending your own documents, not for marketing or bulk faxing.

`from` must be an active fax line on your account. `to` must be a valid 10-digit US or Canadian number. Premium numbers (900, 976), service codes and area codes that are other countries are refused.

The answer is `201` with a `fax_id` once the fax carrier has the job. Sending takes minutes. Read the outcome in `GET /v1/faxes` (`status`), and fetch the document with `GET /v1/media/{fax_id}`.

**Dry run.** `test: true` runs every check (the numbers, the file and its pages, your line, the monthly limit) and sends nothing. It counts toward the 3 a minute, never toward the 50 a day.

**Monthly limit.** Each account may send 1,000 fax pages a month for each fax line on it (unless Vocatech set another figure), pooled across its lines, counted by the Eastern calendar month across the API, the portal and email to fax. Past it, the answer is `429`. Another fax line brings another 1,000 pages a month; contact Vocatech support for a line or a different figure.

### Request

**curl**

```bash
curl -X POST https://api.vocatech.com/v1/faxes \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "7185550100",
    "to": "7185550142",
    "filename": "intake-form.pdf",
    "subject": "Intake form",
    "file": "'"$(base64 < intake-form.pdf | tr -d '\n')"'"
  }'
```

**Node**

```javascript
import { readFile } from "node:fs/promises";

const res = await fetch("https://api.vocatech.com/v1/faxes", {
  method: "POST",
  headers: {
    Authorization: "Bearer YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    from: "7185550100",
    to: "7185550142",
    filename: "intake-form.pdf",
    subject: "Intake form",
    file: (await readFile("intake-form.pdf")).toString("base64"),
  }),
});

console.log(res.status, await res.json());
```

**Python**

```python
import base64
import requests

with open("intake-form.pdf", "rb") as f:
    document = base64.b64encode(f.read()).decode()

r = requests.post(
    "https://api.vocatech.com/v1/faxes",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
    json={
        "from": "7185550100",
        "to": "7185550142",
        "filename": "intake-form.pdf",
        "subject": "Intake form",
        "file": document,
    },
    timeout=60,
)
print(r.status_code, r.json())
```

**PHP**

```php
<?php
require 'vendor/autoload.php';

$client = new GuzzleHttp\Client([
    'base_uri' => 'https://api.vocatech.com',
    'headers' => ['Authorization' => 'Bearer YOUR_API_KEY'],
    'http_errors' => false,
]);

$res = $client->post('/v1/faxes', [
    'json' => [
        'from' => '7185550100',
        'to' => '7185550142',
        'filename' => 'intake-form.pdf',
        'subject' => 'Intake form',
        'file' => base64_encode(file_get_contents('intake-form.pdf')),
    ],
]);

echo $res->getStatusCode(), PHP_EOL, $res->getBody(), PHP_EOL;
```

### Body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `from` | string | yes | One of your active fax lines, 10 digits. Punctuation and a leading 1 are ignored. For example `7185550100`. |
| `to` | string | yes | The recipient's fax number: a valid 10-digit US or Canadian number. Premium numbers (900, 976) and area codes that are other countries are refused. For example `7185550142`. |
| `file` | string (base64) | yes | The document as base64. Send a PDF. Up to 20 MB before encoding. |
| `filename` | string |  | The document's file name. Default `document.pdf`. |
| `subject` | string |  | A subject for your records. It shows in `GET /v1/faxes`. |
| `recipient_name` | string |  | The recipient's name, passed to the fax carrier. |
| `test` | boolean |  | A dry run: check everything and send nothing. Default `false`. |

**Example**

```json
{
  "from": "7185550100",
  "to": "7185550142",
  "filename": "intake-form.pdf",
  "subject": "Intake form",
  "recipient_name": "Jordan Reyes",
  "file": "JVBERi0xLjQKJcfsj6IKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c+PgplbmRvYmoK"
}
```

### Answers

#### 200

A dry run passed every check. Nothing was sent.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `status` | string | yes | Always `test`. |
| `fax` | object | yes |  |
| `fax.from` | string | yes |  |
| `fax.to` | string | yes |  |
| `fax.filename` | string | yes |  |
| `fax.fax_line` | string | yes | The fax line's name in the portal. |
| `fax.valid` | boolean | yes | Always `true`. |

**Example**

```json
{
  "status": "test",
  "fax": {
    "from": "7185550100",
    "to": "7185550142",
    "filename": "intake-form.pdf",
    "fax_line": "Front Desk Fax",
    "valid": true
  }
}
```

#### 201

The fax carrier has the job.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `status` | string | yes | Always `queued`. |
| `fax` | object | yes |  |
| `fax.fax_id` | string | yes | The fax's id. Use it with `GET /v1/media/{id}`. Matches `^fax_[0-9]+$`. |
| `fax.faxage_job_id` | string or null | yes | The fax carrier's job id. |
| `fax.from` | string | yes |  |
| `fax.to` | string | yes |  |
| `fax.filename` | string | yes |  |
| `fax.subject` | string or null | yes |  |
| `fax.created_at` | string | yes | When we took it, ISO 8601 with the Eastern offset. |

**Example**

```json
{
  "status": "queued",
  "fax": {
    "fax_id": "fax_77120",
    "faxage_job_id": "93125501",
    "from": "7185550100",
    "to": "7185550142",
    "filename": "intake-form.pdf",
    "subject": "Intake form",
    "created_at": "2026-09-24T10:05:09-04:00"
  }
}
```

#### 400

A missing field, a number that is not 10 digits, a destination we refuse, or a file that is not base64.

**Example**

```json
{
  "error": {
    "code": 400,
    "type": "BAD_REQUEST",
    "message": "Missing required fields: from, to, and file are required."
  }
}
```

**Example**

```json
{
  "error": {
    "code": 400,
    "type": "BAD_REQUEST",
    "message": "Invalid to number format. Expected 10 digits."
  }
}
```

**Example**

```json
{
  "error": {
    "code": 400,
    "type": "BAD_REQUEST",
    "message": "to may not be a premium-rate number (900)."
  }
}
```

**Example**

```json
{
  "error": {
    "code": 400,
    "type": "BAD_REQUEST",
    "message": "to is outside the US and Canada (area code 876). International numbers are not dialed through the API."
  }
}
```

**Example**

```json
{
  "error": {
    "code": 400,
    "type": "BAD_REQUEST",
    "message": "File must be base64-encoded."
  }
}
```

#### 401

No key, or a key that is wrong or turned off.

#### 403

Refused: faxing is not on for your account, `from` is not an active fax line of yours, the key's scopes, or the address.

**Faxing is off**

```json
{
  "error": {
    "code": 403,
    "type": "FORBIDDEN",
    "message": "Faxing is not enabled for this company."
  }
}
```

**Not your fax line**

```json
{
  "error": {
    "code": 403,
    "type": "FORBIDDEN",
    "message": "Invalid from number. Must be an active fax line assigned to your company."
  }
}
```

#### 413

The document is over 20 MB.

**Example**

```json
{
  "error": {
    "code": 413,
    "type": "ERROR",
    "message": "The document is too large. The limit is 20 MB."
  }
}
```

#### 429

The `faxes` counter is full (3 a minute per account), the account has sent its 50 faxes for the day, or the account's monthly page limit is used. The day and monthly limits answer with an action error and no `Retry-After`.

**Example**

```json
{
  "error": {
    "code": 429,
    "type": "RATE_LIMIT_EXCEEDED",
    "message": "This account has used its 3,000 outbound fax pages for the month, 1,000 pages for each of its 3 fax lines. Contact Vocatech support to raise it."
  }
}
```

#### 500

The fax carrier refused the job or could not be reached. The message is the carrier's error, or the connection error.

**Example**

```json
{
  "error": {
    "code": 500,
    "type": "SERVER_ERROR",
    "message": "cURL error 28: Operation timed out after 60000 milliseconds with 0 bytes received"
  }
}
```

#### 503

Try again shortly. The key could not be checked just now (`Retry-After: 5`), or a service this route needs could not be reached.

## List faxes

`GET /v1/faxes`

Your sent and received faxes in a date window, newest first.

**Dates** work as in `GET /v1/calls`: `start_date` and `end_date` together as local days in `timezone`, or neither for today, at most 366 days. The response echoes the local days you asked for.

**Paging.** `limit` is 500 by default and at most 500. Faxes that tie on time keep a stable order.

`has_document` says whether `GET /v1/media/{fax_id}` can return the PDF. Answers `403` when faxing is not on for your account.

**Known issue.** Fax times are stored in Eastern time. `sent_at` shows that Eastern clock time with a `Z`, so it reads 4 or 5 hours early, and the date window is compared the same way: a fax from midnight to about 4 AM Eastern lands in the previous day's window.

Each request is written to your account's HIPAA access log.

### Request

**curl**

```bash
curl -G https://api.vocatech.com/v1/faxes \
  -H "Authorization: Bearer YOUR_API_KEY" \
  --data-urlencode "start_date=2026-09-24" \
  --data-urlencode "end_date=2026-09-24"
```

**Node**

```javascript
const params = new URLSearchParams({
  start_date: "2026-09-24",
  end_date: "2026-09-24",
});

const res = await fetch(`https://api.vocatech.com/v1/faxes?${params}`, {
  headers: { Authorization: "Bearer YOUR_API_KEY" },
});

console.log(res.status, await res.json());
```

**Python**

```python
import requests

r = requests.get(
    "https://api.vocatech.com/v1/faxes",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
    params={
        "start_date": "2026-09-24",
        "end_date": "2026-09-24",
    },
    timeout=30,
)
print(r.status_code, r.json())
```

**PHP**

```php
<?php
require 'vendor/autoload.php';

$client = new GuzzleHttp\Client([
    'base_uri' => 'https://api.vocatech.com',
    'headers' => ['Authorization' => 'Bearer YOUR_API_KEY'],
    'http_errors' => false,
]);

$res = $client->get('/v1/faxes', [
    'query' => [
        'start_date' => '2026-09-24',
        'end_date' => '2026-09-24',
    ],
]);

echo $res->getStatusCode(), PHP_EOL, $res->getBody(), PHP_EOL;
```

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `start_date` | query | string (date) |  | First local day of the window, `YYYY-MM-DD`, in `timezone`. Send it with `end_date`. Send neither for today. A single date is ignored and you get today. For example `2026-09-01`. |
| `end_date` | query | string (date) |  | Last local day of the window, `YYYY-MM-DD`, included in full. At most 366 days after `start_date`. For example `2026-09-07`. |
| `timezone` | query | string |  | IANA time zone for the dates, for example `America/Chicago`. An unknown zone falls back to `America/New_York`. The response echoes the zone used. Default `America/New_York`. |
| `direction` | query | string |  | `incoming` (received) or `outgoing` (sent). One of `any`, `incoming` or `outgoing`. Default `any`. |
| `status` | query | string |  | Only faxes in this status. One of `any`, `in_progress`, `completed`, `failed` or `restricted`. Default `any`. |
| `search` | query | string |  | Text to find in the other party's number or the subject. |
| `sort` | query | string |  | Order by time. Rows that tie keep a stable order, so paging is safe. One of `day_desc` or `day_asc`. Default `day_desc`. |
| `page` | query | integer |  | Page number, from 1. Default `1`. At least 1. |
| `limit` | query | integer |  | Rows per page, 1 to 500. A value outside that range is brought inside it. Default `500`. From 1 to 500. |

### Answers

#### 200

The faxes in the window.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `query` | object | yes | The filters that were applied, echoed back. |
| `query.company_id` | integer |  |  |
| `query.start_date` | string (date) |  |  |
| `query.end_date` | string (date) |  |  |
| `query.direction` | string |  |  |
| `query.status` | string |  |  |
| `query.search` | string |  |  |
| `query.timezone` | string |  |  |
| `faxes` | array of objects | yes |  |
| `faxes[].fax_id` | string | yes | Use it with `GET /v1/media/{id}` to get the document. Matches `^fax_[0-9]+$`. |
| `faxes[].direction` | string | yes | One of `incoming` or `outgoing`. |
| `faxes[].status` | string | yes | One of `in_progress`, `completed`, `failed`, `restricted` or `unknown`. |
| `faxes[].from` | string or null | yes | Digits without the leading 1. |
| `faxes[].to` | string or null | yes | Digits without the leading 1. |
| `faxes[].pages` | integer or null | yes |  |
| `faxes[].subject` | string or null | yes |  |
| `faxes[].faxage_job_id` | string or null | yes | The fax carrier's job id. |
| `faxes[].has_document` | boolean | yes | True when `GET /v1/media/{fax_id}` can return the PDF. |
| `faxes[].sent_at` | string (date-time) or null | yes | When the fax was logged. Known issue: this is Eastern time written with a `Z`, so it reads 4 or 5 hours early. |
| `meta` | object | yes | Paging for list routes. Each list adds its own total. |
| `meta.page` | integer | yes | This page, from 1. |
| `meta.limit` | integer | yes | Rows per page, after limits were applied. |
| `meta.total_pages` | integer | yes | How many pages there are. |
| `meta.total_faxes` | integer | yes | Faxes that match, across all pages. |

**Example**

```json
{
  "query": {
    "company_id": 1234567890,
    "start_date": "2026-09-24",
    "end_date": "2026-09-24",
    "direction": "any",
    "status": "any",
    "search": "",
    "timezone": "America/New_York"
  },
  "faxes": [
    {
      "fax_id": "fax_77135",
      "direction": "incoming",
      "status": "completed",
      "from": "2125550187",
      "to": "7185550100",
      "pages": 2,
      "subject": null,
      "faxage_job_id": "93125877",
      "has_document": true,
      "sent_at": "2026-09-24T11:40:00.000Z"
    },
    {
      "fax_id": "fax_77120",
      "direction": "outgoing",
      "status": "completed",
      "from": "7185550100",
      "to": "7185550142",
      "pages": 3,
      "subject": "Intake form",
      "faxage_job_id": "93125501",
      "has_document": true,
      "sent_at": "2026-09-24T10:05:09.000Z"
    }
  ],
  "meta": {
    "page": 1,
    "limit": 500,
    "total_pages": 1,
    "total_faxes": 2
  }
}
```

#### 400

A date that is not `YYYY-MM-DD`, an end before the start, or a window over 366 days.

**Example**

```json
{
  "error": {
    "code": 400,
    "type": "BAD_REQUEST",
    "message": "Invalid date format or timezone."
  }
}
```

#### 401

No key, or a key that is wrong or turned off.

#### 403

Faxing is not on for your account, or the key's scopes or address.

**Example**

```json
{
  "error": {
    "code": 403,
    "type": "FORBIDDEN",
    "message": "Faxing is not enabled for this company."
  }
}
```

#### 429

A rate limit was reached. Wait `Retry-After` seconds. The body names the counter, the window and the limit.

#### 500

An unexpected fault on our side, or a search that ran longer than 30 seconds and was stopped.

#### 503

Try again shortly. The key could not be checked just now (`Retry-After: 5`), or a service this route needs could not be reached.
