Message History
The read side of POST /api/v1/fax. Both endpoints are paginated and return
newest first.
GET /api/v1/fax list faxes you may read
GET /api/v1/fax/{fax} read one
Filters. q (fuzzy, across to, from
and status), to, from, status,
direction, date_from and date_to. Punctuation in a
phone number is ignored, so (262) 555-0142 matches +12625550142.
status accepts the stored value (fax.delivered) or the label the
history page shows (Delivered); a label shared by several stored statuses —
Started covers sending, receiving and media processing — matches all of them.
Scope. What you see follows the role you hold in your current organization,
and that is resolved per organization: an Admin in one organization may be a Client in another.
An Admin, Operator or Staff account sees every fax belonging to a member of that
organization, and a Client sees only their own. The organization_id and
user_id parameters are reserved for TechJoy Support and are ignored for
everyone else rather than rejected.
Not found, not forbidden. A fax belonging to another tenant answers
404, exactly as an id that never existed does. Unlike a number or a webhook, a
fax id is a bare key into the table every tenant shares, so a 403 would confirm
the row exists and let the sequence be walked to measure someone else's traffic.
Fields. id, user_id, direction,
status, display_status, from, to,
pages, cost, currency, media_url,
sender_email, failure_reason, delivered_at,
failed_at, created_at, updated_at.
cost is the amount actually billable, so a waived charge reads
0.0000; the waiver itself and its audit trail stay internal. The rate card
behind the charge — and the markup it carries — the carrier's fax identifier and its raw
payload are never returned.
Pagination. per_page, capped at 200.
Webhook Scopes
Webhooks are managed at two scopes. An account-level webhook applies to
every number on the account. A number-level webhook applies to one number.
When both exist, a received fax is delivered to both.
Status updates on a sent fax go to the sending number's webhook, or to the
account-level webhook when the number has none. Each scope holds at most one webhook;
creating a second returns 422.
GET /api/v1/webhooks list account-level webhooks
POST /api/v1/webhooks create an account-level webhook
GET /api/v1/webhooks/{webhook} read one
PUT /api/v1/webhooks/{webhook} replace url + accepts
DELETE /api/v1/webhooks/{webhook} remove it
GET /api/v1/numbers/{number}/webhooks list a number's webhooks
POST /api/v1/numbers/{number}/webhooks create one for that number
GET /api/v1/numbers/{number}/webhooks/{webhook} read one
PUT /api/v1/numbers/{number}/webhooks/{webhook} replace url + accepts
DELETE /api/v1/numbers/{number}/webhooks/{webhook} remove it
Secrets. The plaintext signing secret is returned exactly once — in the
response that creates the webhook. It is never included in list, read or update responses,
and the stored hash is never exposed. Use POST /api/v1/webhook/rotate to issue
a replacement; PUT changes delivery settings only and leaves the secret intact.
Scoping. A number outside your organization answers 403.
A webhook id that exists but belongs to another account, or to a different number than the
one in the path, answers 404.
Compatibility. POST /api/v1/webhooks keeps its original
behaviour when a phone_number is supplied: it registers the webhook against
that number and returns {action, phone_number, url, secret}. Omit
phone_number to create an account-level webhook instead.
Pagination. List endpoints accept per_page, capped at 200.
Signing Webhook Requests
Each webhook request must be signed using HMAC-SHA256 with your webhook_secret.
Requests must include the following headers:
X-Webhook-Timestamp: <timestamp>
X-Webhook-Signature: sha256=<signature>
Steps to sign a request:
1. Retrieve yourwebhook_secret. 2. Serialize the request body to a raw string. 3. Generate a Unix timestamp in seconds. 4. Concatenate<timestamp>.<body>. 5. Compute HMAC-SHA256 of this string usingwebhook_secret. 6. Add the signature and timestamp headers to the request.
JavaScript (Node.js)
const crypto = require('crypto');
function signWebhookRequest(body, webhookSecret) {
const timestamp = Math.floor(Date.now() / 1000).toString();
const payload = `${timestamp}.${body}`;
const signature = crypto.createHmac('sha256', webhookSecret)
.update(payload)
.digest('hex');
return { timestamp, signature: `sha256=${signature}` };
}
// Usage
const body = JSON.stringify({ foo: 'bar' });
const { timestamp, signature } = signWebhookRequest(body, process.env.WEBHOOK_SECRET);
Python
import hmac, hashlib, time
def sign_webhook_request(body: str, secret: str):
timestamp = str(int(time.time()))
payload = f"{timestamp}.{body}"
signature = hmac.new(secret.encode(), payload.encode(), hashlib.sha256).hexdigest()
return timestamp, f"sha256={signature}"
# Usage
body = '{"foo":"bar"}'
timestamp, signature = sign_webhook_request(body, "your_webhook_secret")
PHP
function signWebhookRequest($body, $secret) {
$timestamp = (string) time();
$payload = $timestamp . '.' . $body;
$signature = hash_hmac('sha256', $payload, $secret);
return [
'timestamp' => $timestamp,
'signature' => 'sha256=' . $signature
];
}
// Usage
$body = json_encode(['foo' => 'bar']);
$result = signWebhookRequest($body, getenv('WEBHOOK_SECRET'));
cURL Example
curl -X POST https://your-webhook-url \
-H "Content-Type: application/json" \
-H "X-Webhook-Timestamp: <timestamp>" \
-H "X-Webhook-Signature: sha256=<signature>" \
-d '{"foo":"bar"}'