ifax

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 23, 2026 License: MIT Imports: 17 Imported by: 0

README

INeedToSendAFax Go SDK

Official Go client for the INeedToSendAFax API. Send faxes, track delivery, and verify webhook callbacks.

  • Zero dependencies (standard library only)
  • Context-first, safe for concurrent use
  • Automatic idempotency keys and retries for transient failures
  • Typed errors and webhook signature verification

Install

go get github.com/INeedToSendAFax/sdk-go

The module path ends in sdk-go, but the package name is ifax, so it imports as:

import "github.com/INeedToSendAFax/sdk-go"

Quick start

Grab an API key from api.ineedtosendafax.com and set it as IFAX_API_KEY.

package main

import (
	"context"
	"log"

	ifax "github.com/INeedToSendAFax/sdk-go"
)

func main() {
	client, err := ifax.NewFromEnv()
	if err != nil {
		log.Fatal(err)
	}

	fax, err := client.SendFax(context.Background(), &ifax.SendFaxParams{
		To:    "15551234567",
		Files: []ifax.File{ifax.FilePath("invoice.pdf")},
	})
	if err != nil {
		log.Fatal(err)
	}
	log.Println("queued:", fax.ID)
}

Sending a fax

SendFax accepts PDF, Word (DOC/DOCX), PNG/JPEG, PostScript, and TIFF, up to 10 files combined into one fax and 32 MB each. Pass a callback URL to be notified on completion, and set Cover for a generated cover page.

fax, err := client.SendFax(ctx, &ifax.SendFaxParams{
	To:           "15551234567",
	Files:        []ifax.File{ifax.FilePath("report.pdf"), ifax.FilePath("appendix.pdf")},
	CallbackURL:  "https://example.com/fax-webhook",
	Cover:        true,
	CoverText:    "Confidential",
	SenderName:   "Acme Billing",
	RecipientName: "Dr. Smith",
})

Files can come from memory or any reader:

ifax.FileBytes("note.txt", []byte("hello"))
ifax.FileReader("note.txt", myReader)

Tracking status

fax, err := client.GetFax(ctx, "b1a2c3d4-...")
// fax.Status is queued, sending, sent, or failed.

list, err := client.ListFaxes(ctx, 50)   // list.Data, list.BalanceCents
acct, err := client.Me(ctx)              // acct.BalanceCents, acct.MaxPages, ...
price, err := client.Pricing(ctx)

Verifying webhooks

When you set callback_url, we POST the final status with an X-Fax-Signature header. Verify it against the raw request body using your webhook signing secret.

http.HandleFunc("/fax-webhook", func(w http.ResponseWriter, r *http.Request) {
	body, _ := io.ReadAll(r.Body)

	if err := ifax.VerifyWebhook(secret, r.Header.Get("X-Fax-Signature"), body, 0); err != nil {
		http.Error(w, "bad signature", http.StatusUnauthorized)
		return
	}
	event, _ := ifax.ParseEvent(body)
	log.Printf("fax %s is %s", event.ID, event.Status)
	w.WriteHeader(http.StatusOK)
})

VerifyWebhook uses DefaultTolerance (5 minutes) when passed 0, and uses a constant-time comparison. It returns ErrInvalidSignature, ErrTimestampOutsideTolerance, or ErrMalformedSignatureHeader, so you can tell a bad signature from a replayed request.

Retries and idempotency

The client retries 429 and 5xx responses and network errors with backoff, honoring Retry-After. SendFax always sends an Idempotency-Key (generated when you do not provide one), so a retried send never delivers a fax twice.

client := ifax.New(key, ifax.WithMaxRetries(3), ifax.WithTimeout(90*time.Second))

Errors

All non-2xx responses become an *APIError:

if ifax.IsInsufficientCredits(err) {
	// buy more credit
}
if ifax.IsRateLimited(err) {
	// back off and retry
}
if ifax.IsNotFound(err) {
	// unknown fax id
}

var apiErr *ifax.APIError
if errors.As(err, &apiErr) {
	log.Println(apiErr.StatusCode, apiErr.Code, apiErr.Message)
}

License

MIT

Documentation

Overview

Package ifax is the official Go client for the INeedToSendAFax API.

Send faxes programmatically, track their status, and verify webhook callbacks. The client is safe for concurrent use.

Create a client with your API key and send a fax:

client := ifax.New(os.Getenv("IFAX_API_KEY"))
fax, err := client.SendFax(ctx, &ifax.SendFaxParams{
	To:    "15551234567",
	Files: []ifax.File{ifax.FilePath("invoice.pdf")},
})

See https://api.ineedtosendafax.com/docs for the API reference.

Index

Examples

Constants

View Source
const DefaultBaseURL = "https://api.ineedtosendafax.com"

DefaultBaseURL is the production API endpoint.

View Source
const DefaultTolerance = 5 * time.Minute

DefaultTolerance is the recommended replay window for webhook timestamps.

View Source
const Version = "1.0.0"

Version is the SDK version.

Variables

View Source
var (
	ErrNotFound            = &APIError{StatusCode: http.StatusNotFound, Code: "not_found"}
	ErrInsufficientCredits = &APIError{StatusCode: http.StatusPaymentRequired, Code: "insufficient_credits"}
	ErrRateLimited         = &APIError{StatusCode: http.StatusTooManyRequests, Code: "rate_limited"}
	ErrInvalidKey          = &APIError{StatusCode: http.StatusUnauthorized, Code: "invalid_key"}
)

Sentinel API errors for use with errors.Is.

View Source
var (
	ErrMalformedSignatureHeader  = errors.New("ifax: malformed X-Fax-Signature header")
	ErrInvalidSignature          = errors.New("ifax: webhook signature does not match")
	ErrTimestampOutsideTolerance = errors.New("ifax: webhook timestamp outside tolerance")
)

Webhook verification errors.

View Source
var ErrFileTooLarge = errors.New("ifax: file exceeds 32 MB")

ErrFileTooLarge is returned when a file exceeds 32 MB.

View Source
var ErrNoAPIKey = errors.New("ifax: no API key (set IFAX_API_KEY)")

ErrNoAPIKey is returned by NewFromEnv when no key is present.

Functions

func IsInsufficientCredits

func IsInsufficientCredits(err error) bool

IsInsufficientCredits reports whether err is a 402 (out of credit).

func IsNotFound

func IsNotFound(err error) bool

IsNotFound reports whether err is a 404 from the API.

func IsRateLimited

func IsRateLimited(err error) bool

IsRateLimited reports whether err is a 429.

func VerifyWebhook

func VerifyWebhook(secret, signatureHeader string, body []byte, tolerance time.Duration) error

VerifyWebhook checks the X-Fax-Signature header against the raw request body using your webhook signing secret. Pass the header value verbatim and the unmodified body. A tolerance of 0 uses DefaultTolerance; use a negative value to skip the timestamp check.

Return values are ErrMalformedSignatureHeader, ErrTimestampOutsideTolerance, or ErrInvalidSignature, so callers can distinguish a bad shape from a bad signature from an old replay.

Example

ExampleVerifyWebhook verifies a signed callback, then parses it.

package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"fmt"

	ifax "github.com/INeedToSendAFax/sdk-go"
)

func main() {
	const secret = "whsec_example"
	body := []byte(`{"id":"abc","status":"sent"}`)
	ts := "1700000000"

	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(ts + "."))
	mac.Write(body)
	header := "t=" + ts + ",v1=" + hex.EncodeToString(mac.Sum(nil))

	// A negative tolerance skips the timestamp/replay check for this example.
	if err := ifax.VerifyWebhook(secret, header, body, -1); err != nil {
		fmt.Println("invalid:", err)
		return
	}
	event, _ := ifax.ParseEvent(body)
	fmt.Println("verified", event.ID, event.Status)
}
Output:
verified abc sent
Example (Stale)

ExampleVerifyWebhook_stale rejects an old signature to prevent replays.

package main

import (
	"fmt"
	"time"

	ifax "github.com/INeedToSendAFax/sdk-go"
)

func main() {
	const secret = "whsec_example"
	body := []byte(`{"id":"abc"}`)
	header := "t=1700000000,v1=0000"

	err := ifax.VerifyWebhook(secret, header, body, 5*time.Minute)
	fmt.Println(err == ifax.ErrTimestampOutsideTolerance)
}
Output:
true

Types

type APIError

type APIError struct {
	StatusCode int
	Code       string
	Message    string
	// RetryAfter is set when the API returns a Retry-After header.
	RetryAfter time.Duration
}

APIError is returned for any non-2xx API response.

func (*APIError) Error

func (e *APIError) Error() string

func (*APIError) Is

func (e *APIError) Is(target error) bool

Is reports whether the error is an *APIError with the given code.

type Account

type Account struct {
	KeyPrefix         string `json:"key_prefix"`
	Email             string `json:"email"`
	BalanceCents      int    `json:"balance_cents"`
	PriceCents        int    `json:"price_cents"`
	MaxPages          int    `json:"max_pages"`
	RateLimitPerMin   int    `json:"rate_limit_per_min"`
	DailyCap          int    `json:"daily_cap"`
	FaxesLast24h      int    `json:"faxes_last_24h"`
	CallbackSecretSet bool   `json:"callback_secret_set"`
}

Account is the response from GET /v1/me.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client talks to the INeedToSendAFax API. It is safe for concurrent use; do not modify its fields directly. Construct it with New.

func New

func New(apiKey string, opts ...Option) *Client

New returns a Client using the given API key. Options may override defaults.

func NewFromEnv

func NewFromEnv(opts ...Option) (*Client, error)

NewFromEnv builds a Client from the IFAX_API_KEY environment variable.

func (*Client) GetFax

func (c *Client) GetFax(ctx context.Context, id string) (*Fax, error)

GetFax returns a single fax by id. A 404 is returned as an *APIError that satisfies IsNotFound.

Example

ExampleClient_GetFax polls a fax for its final status.

package main

import (
	"context"
	"fmt"
	"os"

	ifax "github.com/INeedToSendAFax/sdk-go"
)

func main() {
	client := ifax.New(os.Getenv("IFAX_API_KEY"))

	fax, err := client.GetFax(context.Background(), "b1a2c3d4-0000-0000-0000-000000000000")
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	if fax.Status == "sent" {
		fmt.Println("delivered")
	}
}

func (*Client) ListFaxes

func (c *Client) ListFaxes(ctx context.Context, limit int) (*FaxList, error)

ListFaxes returns recent faxes for the account. limit is clamped by the API to 1..200; pass 0 for the default (50).

func (*Client) Me

func (c *Client) Me(ctx context.Context) (*Account, error)

Me returns the account balance, limits and recent usage for the API key.

func (*Client) Pricing

func (c *Client) Pricing(ctx context.Context) (*Pricing, error)

Pricing returns the current price and limits.

func (*Client) SendFax

func (c *Client) SendFax(ctx context.Context, p *SendFaxParams) (*Fax, error)

SendFax submits a fax. It returns a Fax with status "queued"; poll GetFax or use a callback_url to learn when it is delivered. A retried call is safe: if IdempotencyKey is empty one is generated automatically.

Example

ExampleClient_SendFax shows how to send a fax with a callback.

package main

import (
	"context"
	"fmt"
	"os"

	ifax "github.com/INeedToSendAFax/sdk-go"
)

func main() {
	client := ifax.New(os.Getenv("IFAX_API_KEY"))

	fax, err := client.SendFax(context.Background(), &ifax.SendFaxParams{
		To:          "15551234567",
		Files:       []ifax.File{ifax.FilePath("invoice.pdf")},
		CallbackURL: "https://example.com/fax-webhook",
	})
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Println("queued:", fax.ID)
}

type Event

type Event struct {
	ID          string     `json:"id"`
	Status      string     `json:"status"`
	To          string     `json:"to"`
	Pages       int        `json:"pages"`
	Bitrate     int        `json:"bitrate"`
	Mode        string     `json:"mode"`
	Error       string     `json:"error"`
	CompletedAt *time.Time `json:"completed_at"`
}

Event is the JSON body of a fax status callback.

func ParseEvent

func ParseEvent(body []byte) (*Event, error)

ParseEvent decodes a callback body. Call VerifyWebhook first when the request came from the network.

type Fax

type Fax struct {
	ID          string     `json:"id"`
	Status      string     `json:"status"`
	To          string     `json:"to"`
	Pages       int        `json:"pages"`
	PriceCents  int        `json:"price_cents"`
	Bitrate     int        `json:"bitrate"`
	Mode        string     `json:"mode"`
	Error       string     `json:"error"`
	CreatedAt   *time.Time `json:"created_at"`
	CompletedAt *time.Time `json:"completed_at"`
}

Fax is a fax job.

type FaxList

type FaxList struct {
	Data         []Fax `json:"data"`
	BalanceCents int   `json:"balance_cents"`
}

FaxList is a page of recent faxes plus the account balance.

type File

type File struct {
	Filename string
	// contains filtered or unexported fields
}

File is a document to fax. Create one with FilePath, FileBytes or FileReader. Files are read into memory when the fax is sent so that retries can replay the request body.

func FileBytes

func FileBytes(name string, b []byte) File

FileBytes wraps an in-memory document.

func FilePath

func FilePath(path string) File

FilePath references a file on disk, read when the fax is sent.

func FileReader

func FileReader(name string, r io.Reader) File

FileReader wraps an io.Reader; it is read fully when the fax is sent.

type Option

type Option func(*Client)

Option configures a Client.

func WithBaseURL

func WithBaseURL(u string) Option

WithBaseURL overrides the API base URL (useful for testing).

func WithHTTPClient

func WithHTTPClient(h *http.Client) Option

WithHTTPClient uses a custom *http.Client.

func WithMaxRetries

func WithMaxRetries(n int) Option

WithMaxRetries sets how many times transient failures (429 and 5xx responses, network errors) are retried. The default is 2.

func WithTimeout

func WithTimeout(d time.Duration) Option

WithTimeout sets the request timeout on a default HTTP client. It is ignored when WithHTTPClient is also supplied.

type Pricing

type Pricing struct {
	PriceCents        int      `json:"price_cents"`
	Currency          string   `json:"currency"`
	MaxPages          int      `json:"max_pages"`
	MinCreditCents    int      `json:"min_credit_cents"`
	Countries         []string `json:"countries"`
	CoverAvailable    bool     `json:"cover_available"`
	Callbacks         bool     `json:"callbacks"`
	IdempotencyHeader string   `json:"idempotency_header"`
}

Pricing is the response from GET /v1/pricing.

type SendFaxParams

type SendFaxParams struct {
	To    string
	Files []File

	CallbackURL string
	Cover       bool
	CoverText   string

	RecipientName    string
	RecipientCompany string
	SenderName       string
	SenderCompany    string
	SenderPhone      string

	// IdempotencyKey makes retries safe. If empty, one is generated
	// automatically (a retried send will not deliver the fax twice).
	IdempotencyKey string
}

SendFaxParams describes a fax to send. To and Files are required.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL