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.

Metadata

Release files for nfsn-cli 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for nfsn-cli 0.1.1
File Size Uploaded
nfsn_cli-0.1.1.tar.gz 51.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for nfsn-cli 0.1.1
File Interpreter ABI Platform
nfsn_cli-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 79.3 kB

Release files / nfsn_cli-0.1.1.tar.gz

Download URL nfsn_cli-0.1.1.tar.gz
Size 51.9 kB
Tags Source
SHA-256 checksum
How to use checksums
2321aeb050186d09a4ca448b5698a0e44a68c80b3020fcb6f86c6a59ef276ad3
BLAKE2b-256 checksum
How to use checksums
3c48c91463adc7fb460c96423392fcfe6f9d7700ce5a6ecafe1b821739ee297b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 18, 2026.

Transparency log

Release files / nfsn_cli-0.1.1-py3-none-any.whl

Download URL nfsn_cli-0.1.1-py3-none-any.whl
Size 27.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
30405f2f7b5f7b44fd27d4bc29824dd5c43a836c72826605d6694cf9f8ca6198
BLAKE2b-256 checksum
How to use checksums
c00c0454c1bcf81c582536fadb387f531f8832c85cbd4e30fa22060c04a41fcb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 18, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files

0.1.0

2 release 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