Skip to main content

nfsn-cli

PyPI Python License

A modern command line interface and Python client for the NearlyFreeSpeech.NET API.

Covers the full documented API surface — Account, DNS, Email, Member and Site — plus a declarative, plan-and-apply workflow for DNS zones.

pip install nfsn-cli
nfsn init
nfsn dns list example.com

Why another one

The existing Python client, python-nfsn, was last committed in June 2018, targets Python 2.6–3.5, and depends on beanbag. It also predates several API members: balanceCash, balanceCredit, balanceHigh, replaceRR, and the sync property are all missing from it.

nfsn-cli is Python 3.11+, has no legacy baggage, ships a real CLI rather than a debug wrapper, and — importantly — encodes several API behaviours that are not in NFSN's documentation and that a naive client gets wrong. See MX records.

It reads ~/.nfsn-api if you already have one, so migrating costs nothing.

Configuration

nfsn init                      # writes ~/.config/nfsn/credentials, mode 0600
$EDITOR ~/.config/nfsn/credentials

Generate the API key in the member panel: Profile → Actions → Set/Change API Key.

NFSN_LOGIN=yourlogin
NFSN_API_KEY=...

Credentials are resolved in this order:

  1. NFSN_LOGIN / NFSN_API_KEY environment variables
  2. ~/.config/nfsn/credentials
  3. ~/.nfsn-api (the JSON file NFSN's Perl library established)

NFSN rejects any request whose timestamp is more than 5 seconds from its own clock. nfsn-cli detects this from the response Date header and tells you to sync your clock, rather than reporting a misleading authentication failure.

Commands

nfsn dns      list, props, get, set, add, remove, replace, update-serial, sync,
              export, plan, apply
nfsn account  show, get, set-name, add-site, add-warning, remove-warning
nfsn email    forwards, set-forward, remove-forward
nfsn member   show
nfsn site     add-alias, remove-alias

Every mutating command prompts for confirmation; pass --yes to skip it. Read commands accept --json.

Declarative DNS

nfsn dns export example.com -o zone.yaml   # dump live state
$EDITOR zone.yaml
nfsn dns plan zone.yaml                    # diff, no changes made
nfsn dns apply zone.yaml --yes             # execute
nfsn dns apply zone.yaml --yes --wait      # ...and poll until fully propagated

apply without --yes is a dry run.

domain: example.com
records:
  - name: ""                                  # "" or "@" is the apex
    type: TXT
    data: v=spf1 include:amazonses.com ~all
    ttl: 3600
  - name: mail
    type: MX
    data: feedback-smtp.us-west-2.amazonses.com.
    aux: 10                                   # priority; see below

Safety model

By default apply only touches record sets the zone file actually names — a file listing three DKIM CNAMEs cannot delete your MX records, whatever else is in the zone. Anything outside those name+type pairs is reported as untouched and left alone.

--prune makes the file authoritative for the whole zone and deletes everything not listed. Run plan --prune first, every time.

Records NFSN owns (scope other than member) are never proposed for removal, even under --prune, and the client refuses to try.

TTL-only differences are not treated as changes; a delete/recreate cycle is not worth it.

One sharp edge: if the file declares an apex TXT for SPF and the apex also carries unrelated TXT records (domain verification tokens, say), those share the name+type pair and are considered managed — so they show up as removals. Read the plan.

MX records: read this

NFSN uses three different shapes for the same MX record across three verbs. None of this is documented; all of it was verified live against the API on 2026-08-17.

Verb data shape Priority
addRR "10 mail.example.com." prefix on data; there is no aux parameter
listRRs "mail.example.com." separate aux field
removeRR "mail.example.com." matched without the prefix — the joined form 404s

The consequences of getting this wrong are not subtle:

  • Send the bare hostname to addRR and you create an MX record with no priority.
  • Send the joined form to removeRR and the deletion silently 404s, so a "replace" leaves the old record in place and adds a second one beside it.

nfsn-cli handles the conversion for you. Write the priority whichever way you like — the joined form everyone reaches for, or an explicit aux — and it is normalized on the way in:

nfsn dns add example.com MX "10 mail.example.com."          # joined, as the web UI takes it
nfsn dns add example.com MX mail.example.com. --aux 10      # split, as listRRs returns it

Both produce the same record, and remove accepts either too. Giving the priority twice is an error rather than a silently malformed 10 10 mail.example.com..

Internally the split form wins: data is the bare target, aux the priority, and Record.wire_data re-joins them for addRR. export always emits the split shape, so round-trips are stable, and load_zone rejects an MX or SRV record with no priority at all rather than guessing a default.

CNAME exclusivity

NFSN's guidance is that a CNAME "must be the only record present for a given value of the Name field. If this rule is not observed, the results are undefined." The API does not enforce it. plan and apply do — checked against the zone as it would exist after the plan, so a collision with an already-published record your zone file never mentions is caught too. A plan that removes the conflicting record in the same run is still allowed.

replaceRR supports only A, AAAA and TXT, so it sidesteps the issue entirely — but that also means it cannot be used for MX or CNAME.

Python API

from nfsn_cli import Nfsn, NfsnTransport, Record
from nfsn_cli.config import load_credentials

credentials, _warnings = load_credentials()
with NfsnTransport(credentials) as transport:
    nfsn = Nfsn(transport)

    for record in nfsn.dns("example.com").list_rrs():
        print(record.display_name("example.com"), record.type, record.describe())

    print(nfsn.account("A1B2-C3D4E5F6").balance)
    print(nfsn.dns("example.com").sync)  # 0.0-1.0 propagation fraction

API coverage

Mirrors NFSN's API reference.

Object Properties Methods
Account balance, balanceCash, balanceCredit, balanceHigh, friendlyName (r/w), status, sites addSite, addWarning, removeWarning
DNS expire, minTTL, refresh, retry, serial, sync (read-only) addRR, listRRs, removeRR, replaceRR, updateSerial
Email listForwards, setForward, removeForward
Member accounts, sites
Site addAlias, removeAlias

NFSN's Introduction page also names a Database object type, but its reference section documents no members, so it is not implemented.

Development

uv sync
scripts/install-hooks.sh
uv run pytest
uv run ruff check . && uv run ruff format --check .

See CONTRIBUTING.md — in particular the note on why mocked tests are not sufficient when changing how records are read or written.

License

MIT. Not affiliated with or endorsed by NFSN, Inc.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

nfsn_cli-0.1.1.tar.gz (51.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

nfsn_cli-0.1.1-py3-none-any.whl (27.4 kB view details)

Uploaded Python 3

File details

Details for the file nfsn_cli-0.1.1.tar.gz.

File metadata

  • Download URL: nfsn_cli-0.1.1.tar.gz
  • Upload date:
  • Size: 51.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for nfsn_cli-0.1.1.tar.gz
Algorithm Hash digest
SHA256 2321aeb050186d09a4ca448b5698a0e44a68c80b3020fcb6f86c6a59ef276ad3
MD5 63c26927b378d55ad45823f0177f5c1a
BLAKE2b-256 3c48c91463adc7fb460c96423392fcfe6f9d7700ce5a6ecafe1b821739ee297b

See more details on using hashes here.

Provenance

The following attestation bundles were made for nfsn_cli-0.1.1.tar.gz:

Publisher: release.yml on cfdude/nfsn-cli

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file nfsn_cli-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: nfsn_cli-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 27.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for nfsn_cli-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 30405f2f7b5f7b44fd27d4bc29824dd5c43a836c72826605d6694cf9f8ca6198
MD5 cc734284896329649cf521c178942b3d
BLAKE2b-256 c00c0454c1bcf81c582536fadb387f531f8832c85cbd4e30fa22060c04a41fcb

See more details on using hashes here.

Provenance

The following attestation bundles were made for nfsn_cli-0.1.1-py3-none-any.whl:

Publisher: release.yml on cfdude/nfsn-cli

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page