TechJoy Fax API home TechJoy Fax API

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 your webhook_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 using webhook_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"}'