Skip to content
Developers

A fax API that is scoped to one organization and cannot be talked out of it.

An API key belongs to exactly one organization. There is no parameter that selects a different one, so the tenant boundary is the same for the API as it is for the interface.

Authentication

Bearer tokens, hashed at rest

Keys are created by an Administrator in Settings. The secret is shown once, at creation; VitalFax stores only a keyed hash of it and cannot recover the original. Each key carries explicit scopes, a creation record and a last-used timestamp, and can be revoked without affecting any other key.

Send the key as a bearer token. Requests without one, or with a revoked or expired one, are refused and the refusal is recorded.

Scopes

  • fax:read
  • fax:send
  • documents:read
  • documents:write
  • contacts:read
  • contacts:write
  • exchange:read
  • exchange:send
  • webhooks:manage
Authenticatebash
curl https://app.vitalfax.com/api/v1/faxes \
  -H "Authorization: Bearer vf_live_..." \
  -H "Accept: application/json"

Endpoints

What the API covers

The initial surface mirrors what the interface does most often.

MethodPathScopeDescription
GET/api/v1/me—The organization this key belongs to, and its scopes.
GET/api/v1/faxesfax:readList fax, filtered by direction and status.
GET/api/v1/faxes/{id}fax:readOne fax with its delivery history.
POST/api/v1/faxesfax:sendQueue an outbound fax from stored documents.
GET/api/v1/numbersfax:readFax numbers assigned to the organization.
GET/api/v1/documentsdocuments:readList documents with search and filters.
GET/api/v1/documents/{id}documents:readDocument metadata.
GET/api/v1/documents/{id}/contentdocuments:readDocument bytes. The access is audited.
POST/api/v1/documentsdocuments:writeUpload a document.
GET/api/v1/contactscontacts:readList contacts.
POST/api/v1/exchangesexchange:sendCreate a secure exchange and send links.
GET/api/v1/webhookswebhooks:manageList webhook endpoints.
Send a faxbash
curl -X POST https://app.vitalfax.com/api/v1/faxes \
  -H "Authorization: Bearer vf_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "faxNumberId": "fxn_...",
    "toNumber": "+14155550177",
    "recipientName": "Records department",
    "subject": "Referral packet 4821",
    "documentIds": ["doc_...", "doc_..."],
    "includeCoverPage": true
  }'
Responsejson
{
  "id": "fax_2f8k...",
  "status": "queued",
  "direction": "outbound",
  "pageCount": 12,
  "createdAt": "2026-09-16T14:02:11.402Z"
}

Retries

Send the same fax twice by accident and we will not send it twice

A request that times out leaves you with no way to know whether it landed. Send an Idempotency-Key and you can retry safely.

Put a unique value in an Idempotency-Key header on any POST. A UUID per logical operation is the usual choice — one per fax you intend to send, reused across every retry of that one fax.

The first request runs. Any later request with the same key returns the same response as the first, with idempotency-replayed: true so you can tell which call did the work. Nothing is sent a second time.

Reusing a key with a different body returns 409 rather than the earlier response: the request you just made is not the one we would be answering. A retry that arrives while the first is still running also returns 409 — wait a moment and try again.

Keys are remembered for 24 hours and are scoped to your organization, so your choice of key cannot collide with anyone else’s. Without the header every request is independent — which for a fax means a retry sends the document again.

Retry safelybash
# Same key, run twice. One fax leaves the building.
curl -X POST https://app.vitalfax.com/api/v1/faxes \
  -H "Authorization: Bearer vf_live_..." \
  -H "Idempotency-Key: 8f14e45f-ea1b-4c2f-9a7b-2d1c0a5e7b31" \
  -H "Content-Type: application/json" \
  -d '{ "faxNumberId": "fxn_...", "toNumber": "+14155550177",
        "documentIds": ["doc_..."] }'

Limits

Rate limits tell you when to come back

Every response carries the limit, what is left of it, and when it resets.

Each response includes x-ratelimit-limit, x-ratelimit-remaining and x-ratelimit-reset (a Unix timestamp). A 429 also carries retry-after in seconds — wait that long rather than guessing.

Limits are per API key. Sending fax is held to a lower limit than reading, because a burst of reads costs nobody anything and a burst of sends costs you money.

Every response also carries an x-request-id. Quote it when you contact support and we can find the exact request.

Headerstext
x-ratelimit-limit: 60
x-ratelimit-remaining: 57
x-ratelimit-reset: 1793635200
x-request-id: 4e1f2a08-...

# on 429
retry-after: 23

Webhooks

Signed, timestamped, and free of clinical detail

VitalFax tells your system that something happened. It never puts the contents of a document in a webhook payload.

Each request carries a vitalfax-signature header of the form t=…,v1=…, where v1 is an HMAC-SHA256 of {timestamp}.{body} using your endpoint’s signing secret. Verify the signature and reject requests with an old timestamp.

Deliveries retry with exponential backoff. An endpoint that fails persistently is disabled rather than retried indefinitely, and you can replay a delivery from the product.

Events

  • fax.received
  • fax.delivered
  • fax.failed
  • document.created
  • exchange.opened
  • exchange.downloaded
Verify a webhook (Node)javascript
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(
    header.split(',').map((p) => p.split('=')),
  );
  const age = Math.abs(Date.now() / 1000 - Number(parts.t));
  if (!Number.isFinite(age) || age > 300) return false;

  const expected = createHmac('sha256', secret)
    .update(`${parts.t}.${rawBody}`)
    .digest('hex');

  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1 ?? '');
  return a.length === b.length && timingSafeEqual(a, b);
}

Payloads carry identifiers, not content

A fax.received event tells you a fax arrived, its id, its page count and which number received it. To read the document, call the API with your own credentials. This is enforced in code: the webhook emitter rejects payloads containing content-bearing fields.

Conventions

Things worth knowing

Errors

Every error returns a machine code and a human message: unauthenticated, forbidden, not_found, validation_failed, rate_limited, provider_unavailable.

Not found means not yours

A request for an id belonging to another organization returns 404, not 403. A different answer would confirm the record exists.

Pagination

List endpoints take page and pageSize, and return total, totalPages, hasNext and hasPrevious. Page size is capped at 100.

Rate limits

Limits are applied per key and per bucket. Exceeding one returns 429 with the time to wait in the message.

Numbers are E.164

Send +14155550177. Ten-digit North American numbers are accepted and normalized; anything ambiguous is rejected rather than guessed.

Idempotent inbound

Provider events are de-duplicated by event id, so a redelivered inbound fax never creates a second document.

Full reference documentation ships with the product and is kept in the repository alongside the code. Ask us for it.