Skip to main content

nfsn-cli

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: Record.wire_data produces the joined form for addRR, and remove_rr sends the split form. Zone files always use the split shape (data plus aux), matching what export gives you, and load_zone rejects an MX or SRV record with no aux rather than guessing a default.

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.0.tar.gz (48.2 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.0-py3-none-any.whl (25.6 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: nfsn_cli-0.1.0.tar.gz
  • Upload date:
  • Size: 48.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.5.24

File hashes

Hashes for nfsn_cli-0.1.0.tar.gz
Algorithm Hash digest
SHA256 458faed047de39bd4452d4b1795db52eb7381ee8024b24f897965efdb01b1ed2
MD5 b49e1148c0927299ad8ee0cf696f8371
BLAKE2b-256 7d53dcbac01f096d59d2ba16944cf9d0a75f2fa4413d59fe992a3a13c4b87228

See more details on using hashes here.

File details

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

File metadata

  • Download URL: nfsn_cli-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 25.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.5.24

File hashes

Hashes for nfsn_cli-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a34834a4026bb2f283221a02ee8f1e6c3444092111a036c96fdf779789cbc57a
MD5 03387d890beeff6e96c53eb20075877d
BLAKE2b-256 f2bba2f268e1f0ddb52926da3061318682f013ed836c6a226fdd56563f9ffe30

See more details on using hashes here.

Release history Release notifications | RSS feed

0.1.1

2 files

This release

0.1.0 This release

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