Inky Bird Frame reads public observations, your private exported eBird history, detections from your own acoustic station through BirdWeather or self-hosted BirdNET-Go, private CSV history from BirdNET Analyzer, authorized postcard detections from your own Bird Buddy account, or a combination. Every result enters the same catalog and review process.
Merlin Bird ID is Cornell's identification app. Its nearby lists are powered by eBird, so this project uses the documented eBird API rather than trying to automate Merlin.
| Source | Credentials | Windows | Best use |
|---|---|---|---|
inaturalist |
None | All | Default setup, historical seeds, and licensed references |
ebird |
Personal eBird key | 1, 7, or 30 days | Bird-specific recent public sightings |
ebird-archive |
Explicit personal-data import | All | Species from your complete personal eBird history |
birdweather |
BirdWeather station token | All | Species acoustically detected by one station |
birdnet-go |
Local BirdNET-Go base URL | All | Species acoustically detected by a self-hosted station |
birdnet-analyzer |
Explicit CSV import | All; dated rows for finite windows | Offline acoustic-analysis history |
birdbuddy |
Authorized Bird Buddy account | All after setup | Postcard species from one feeder, plus optional manual sightings |
Select any combination with a TOML array. Each configured provider runs independently.
Use the same setup loop for every source:
- Meet the provider's prerequisites and add its explicit name to
discovery.sources. - Run
inky-bird-frame config validate --config /path/to/config.toml. - Run
discoverto inspect provider results without changing the active catalog, then runrefreshto save a successful snapshot. - Inspect the named entry in the
providersarray returned bydiscoverandrefresh. Usestatusseparately for catalog and generation state.
To disable a provider, remove its name from discovery.sources, validate the
configuration, and refresh. Imported history and authentication state are
retained unless a provider section below documents an explicit removal step.
iNaturalist and eBird need a point and radius. Choose exactly one location form; eBird Archive-, BirdWeather-, BirdNET-Go-, BirdNET Analyzer-, and Bird Buddy-only setups need none.
Direct coordinates are the provider-independent option. They work worldwide, do not disclose a postal code to a geocoder, and cannot become ambiguous:
[discovery]
latitude = 0.0
longitude = 0.0For postal-code convenience, use Geoapify's Postcode API. It supports ISO 3166-1 alpha-2 country codes and international postal formats. Create a Geoapify key and keep it in the private configuration:
[discovery]
postal_code = "YOUR_POSTAL_CODE"
country_code = "XX"
geoapify_api_key = "YOUR_PRIVATE_GEOAPIFY_API_KEY"For commands you run yourself, geoapify_api_key_env = "GEOAPIFY_API_KEY" is
also supported. Managed LaunchAgent and systemd services do not inherit your
installation shell, so use the direct key in their mode-0600 configuration.
Geoapify receives the configured country and postal code. The application
requires an exact country/postal-code match and rejects multiple coordinate
results rather than guessing. Geoapify permits stored results and requires
attribution; discovery output includes both Geoapify and source attribution.
Existing zip_code = "12345" configurations remain supported unchanged through
Zippopotam's US endpoint and need no key. This compatibility path is US-only;
new worldwide configurations should use coordinates or Geoapify. No discovery
location is written to a public catalog or rendered on a plate.
Verify a Geoapify-backed location with config validate and discover. An
authorization error points to the API key. An exact-match error usually means
the country code or postal format is wrong; a multiple-match error is
deliberately not guessed. Use direct coordinates when a postal code cannot be
resolved uniquely. To stop using Geoapify, replace the postal-code fields with
direct coordinates, then remove the key.
iNaturalist needs no credentials. The nearby eBird provider requires a personal API key and supports only the recent windows shown in the provider table.
Request a personal key from eBird. Store it in the private configuration:
[discovery]
sources = ["inaturalist", "ebird"]
zip_code = "12345"
radius_km = 8
species_limit = 50
window = "last-30-days"
ebird_api_key = "your-personal-api-key"For commands you run yourself, you may replace ebird_api_key with
ebird_api_key_env = "EBIRD_API_KEY". Native LaunchAgent and systemd services
do not inherit your installation shell, so those installations need the key in
the private mode-0600 configuration file.
Keep the configuration outside the checkout with mode 0600. The application
never writes the key to state, logs, catalog files, or command output.
Verify both sources with discover, then inspect the inaturalist and ebird
entries in providers. Location, network, or upstream API failures stay
provider-specific when another configured source succeeds. An eBird
authentication failure usually means the key is missing or invalid; an
unresolved_count means returned taxonomy could not be matched exactly to an
active iNaturalist bird species. Remove either provider from sources to
disable it; no provider account or remote data is changed.
eBird Archive is optional and separate from the recent-nearby ebird API
provider. Sign in to eBird's official
Download My Data page and request your
complete personal export. Cornell sends a ZIP containing MyEBirdData.csv;
Inky also accepts that CSV directly if you extract or rename it.
The CSV must contain the complete official header and may be at most 512 MiB after decompression. This bound protects the controller from damaged or hostile archives while accommodating very large personal histories.
Preview the complete export before replacing local history:
inky-bird-frame ebird archive import \
--config /path/to/config.toml \
--archive /path/to/ebird.zip \
--dry-runThen import it and opt in to the provider:
inky-bird-frame ebird archive import \
--config /path/to/config.toml \
--archive /path/to/ebird.zip[discovery]
sources = ["ebird-archive"]
window = "all-time"In a mixed-provider installation, keep the normal discovery window for live
sources and seed the complete personal history separately. Preview first, then
repeat without --dry-run:
inky-bird-frame seed --config /path/to/config.toml \
--source ebird-archive --window all-time --species-limit 100 --dry-runThat adds the resolved archive species to the private collection and queues only missing plates; it does not change the configured window for eBird, BirdWeather, or other live providers. An explicit personal travel interval is also supported:
inky-bird-frame seed --config /path/to/config.toml \
--source ebird-archive --start-date 2025-04-29 --end-date 2025-05-02 \
--species-limit 100 --dry-runChoose a species limit appropriate to the size of the export. The command reports the applied limit and discovered count so a truncated selection is visible before it writes collection or queue state.
Each import is a complete snapshot. Reimporting the same export is idempotent;
a newer complete export atomically adds or updates checklists. Inky refuses an
export that omits a previously imported checklist because a filtered download
could otherwise erase history. After confirming that the file is complete and
the reduction reflects intentional eBird edits or deletions, rerun with
--allow-history-reduction.
Private state retains only a one-way checklist fingerprint, the checklist date,
and common and scientific species names. It does not retain eBird account data,
raw submission or location IDs, coordinates, place names, observer counts,
individual bird counts, comments, effort, or media references. One species on
one checklist counts as one personal observation regardless of the reported
number of individuals. ebird archive status reports only aggregate counts and
the imported date range.
Every scientific name must still resolve exactly to one active iNaturalist bird
species. Hybrids, sp. aggregates, domestic groups, and ambiguous or unmatched
labels remain private unresolved diagnostics and cannot trigger generation.
First-time taxonomy resolution is paced to iNaturalist's official guidance of
about one API request per second; large archives can therefore take a few
minutes to seed. Completed matches are cached atomically, so a transient error
or rate limit preserves progress for the next run.
Because the export supplies calendar dates rather than event instants, archive
observations do not claim latest-detection rotation priority. Re-export and
reimport periodically to include newly published eBird checklists; no eBird
password, browser cookie, or unsupported live API is stored.
Verify an import with:
inky-bird-frame ebird archive status --config /path/to/config.toml
inky-bird-frame discover --config /path/to/config.tomlMalformed headers, an oversized uncompressed CSV, or an incomplete replacement
fail before history is committed. Remove ebird-archive from sources to
disable discovery without deleting the private import.
BirdWeather is optional. Create a BirdWeather account and station, then connect a compatible detector such as BirdNET-Pi by following the official BirdNET-Pi integration guide. Copy the station authentication token into the private controller configuration:
[discovery]
sources = ["birdweather"]
radius_km = 8
species_limit = 50
window = "last-30-days"
birdweather_token = "your-station-token"Use sources = ["inaturalist", "ebird", "birdweather"] to combine this station
with nearby iNaturalist and eBird observations. For manually invoked commands,
birdweather_token_env = "BIRDWEATHER_TOKEN" is also supported. Managed
services require the direct token in the private mode-0600 file because they
do not inherit the installation shell environment.
The token authenticates one station. The application uses BirdWeather's
documented station-species endpoint, requests only the avian classification,
and uses the selected time window. It does not query nearby BirdWeather
stations or infer a station from a discovery location. A BirdWeather-only
configuration does not require coordinates or a postal code.
Inky Bird Frame supports:
- reading species names, detection counts, and latest-detection timestamps from the authenticated station;
- exact scientific-name matching to the canonical iNaturalist taxon;
- provider-specific health reporting, retries, and notifications;
- approved catalog reuse, generation, review, rotation, and publication; and
- every configured observation window, up to BirdWeather's 100-species API cap.
Inky Bird Frame does not:
- install, configure, update, or monitor BirdNET-Pi or other acoustic detectors;
- configure microphones, recording schedules, storage, retention, or uploads;
- download, proxy, play, retain, or independently review soundscape audio;
- confirm that a machine classification represents a bird physically present;
- correct detector confidence, placement, background-noise, or taxonomy errors; or
- submit detections to eBird or iNaturalist.
An acoustic detection is a model classification, not a human-confirmed sighting. False positives, overlapping calls, recordings, rebroadcast audio, distant sounds, and detector configuration can affect the result. Inky Bird Frame uses the station's accepted BirdWeather species summary as supplied. Tune and review the detector in its own software before relying on those species for display.
Verify the provider with discover and inspect the birdweather result,
including species_count and unresolved_count. Authentication errors point to
the station token; an empty healthy result can mean that the configured time
window contains no accepted station species. Correct detection policy and
false positives at the detector. Remove birdweather from sources to disable
it; Inky does not change the station.
BirdNET-Go is optional. Point Inky at a BirdNET-Go server reachable from the controller:
[discovery]
sources = ["birdnet-go"]
species_limit = 50
window = "last-30-days"
birdnet_go_url = "http://birdnet-go.local:8080"The URL may use HTTP on a trusted local network or HTTPS through a reverse proxy. The endpoint must be reachable without HTTP authentication; keep it on a trusted network or VPN. Do not embed credentials, query parameters, or fragments. A BirdNET-Go-only configuration needs no discovery location.
On macOS 15 and later, Local Network privacy applies to LaunchAgents. A direct
BirdNET-Go address on the controller's attached subnet can therefore work from
Terminal but fail during the scheduled refresh with [Errno 65] No route to host. Apple documents the distinction between command-line tools and
LaunchAgents in TN3179: Understanding local network privacy.
If you already run a reverse proxy on a routed network path, prefer its HTTPS base URL:
[discovery]
sources = ["birdnet-go"]
birdnet_go_url = "https://birdnet.example.net"The proxy must expose the same unauthenticated BirdNET-Go API. After changing the URL, test the installed service instead of relying on a manual refresh:
refresh_label="gui/$(id -u)/com.inky-bird-frame.refresh"
refresh_pid="$(launchctl kickstart -kp "$refresh_label")"
while kill -0 "$refresh_pid" 2>/dev/null; do sleep 1; done
tail -n 200 "$HOME/Library/Application Support/Inky Bird Frame/logs/refresh.log"The newest result should show the birdnet-go provider with status set to
ok. If both the direct address and the proxy fail from LaunchAgent context,
follow Apple's current Local Network privacy guidance rather than adding a
broad network exception without reviewing its system-wide effect.
Inky uses BirdNET-Go's documented read-only
/api/v2/analytics/species/summary endpoint. It supplies the selected date
window and species limit, then reads only names, counts, and the last-heard
timestamp. It does not download recordings, access media, change settings, or
control the detector. BirdNET-Go applies the station's configured detection
policy and excludes detections marked false-positive before producing the
summary; Inky does not layer on an unexplained second confidence threshold.
Every scientific name must still resolve exactly to one active iNaturalist bird
species. Custom classifier labels, non-birds, and ambiguous or unmatched names
remain private unresolved diagnostics and cannot trigger plate generation. A
BirdNET-Go failure degrades only this provider; other configured sources keep
running. BirdNET-Go runs only when you include birdnet-go in the explicit
sources array.
Verify with discover and inspect the birdnet-go provider. Connection errors
usually mean the base URL is not reachable from the controller process or
container; response-contract errors can indicate an incompatible BirdNET-Go
version. Correct false positives in BirdNET-Go. Remove birdnet-go from
sources to disable it; Inky has no detector-side state to remove.
BirdNET Analyzer is the offline desktop and command-line analyzer from the BirdNET team. Its official CSV output contains segment offsets, species names, confidence, and a source-file field, but no absolute recording date. Inky never guesses one from a filename or filesystem timestamp.
For a directory of recordings, ask Analyzer to create one importable table in addition to its per-file results:
python3 -m birdnet_analyzer.analyze /path/to/recordings \
--output /path/to/analyzer-output \
--rtype csv \
--combine_results \
--min_conf 0.7This example uses a minimum confidence of 0.70. Choose a threshold that fits
your detector policy, then review BirdNET_CombinedTable.csv before import.
Inky accepts one CSV per import and does not apply a second confidence filter.
The required headers are exactly Start (s), End (s), Scientific name,
Common name, Confidence, and File.
Import an Analyzer CSV into controller-private history, then opt in to the provider:
inky-bird-frame birdnet-analyzer import \
--config /path/to/config.toml \
--csv /path/to/analyzer-output/BirdNET_CombinedTable.csv[discovery]
sources = ["birdnet-analyzer"]
window = "all-time"An undated import is eligible only for all-time. When every row in a CSV came
from recordings made on one known calendar date, supply it explicitly:
inky-bird-frame birdnet-analyzer import \
--config /path/to/config.toml \
--csv /path/to/results.csv \
--observed-on 2026-08-09The date applies to every row. Split multi-day results into one CSV per date; do not assign an approximate date merely to make detections enter a shorter window. Analyzer provides no absolute time, so dated imports do not invent a latest-detection timestamp or take latest-detection rotation priority.
Imports are atomic and idempotent. Inky identifies a recording segment from
the CSV's File, Start (s), and End (s) values, allowing a later Analyzer
classification for that same segment to replace the earlier one. Keep the
Analyzer File values stable and unique across your corpus. Reusing one segment
identity with two different explicit dates fails closed rather than silently
mixing history. Use --dry-run to validate and preview counts without replacing
history.
The private mode-0600 state retains only a one-way segment fingerprint,
species names, and an optional explicit date. Source paths, segment offsets,
confidence values, and audio are not retained. Every species still requires an
exact active iNaturalist bird match; unresolved or non-bird labels cannot enter
generation. all-time means all unique segments imported into this
installation, not the complete history of the recording device.
Inky validates that each CSV confidence is a finite value from zero through one but does not impose a second, arbitrary threshold. Set the desired minimum confidence when running BirdNET Analyzer and review its output before import; Inky uses the rows in that export as the operator-approved detection set.
Use --dry-run first, then verify the committed import with discover and
inspect the birdnet-analyzer provider details. Missing or renamed headers fail
closed; Analyzer versions or export modes that produce another schema are not
silently inferred. Remove birdnet-analyzer from sources to disable it
without deleting imported history.
Bird Buddy is optional and uses the private, undocumented GraphQL API behind the Bird Buddy app. It is not an official developer integration and may change without notice. Bird Buddy's current app EULA restricts automated and non-app access. Obtain Bird Buddy's permission for your account before enabling this provider. Inky Bird Frame cannot grant or verify that permission; you are responsible for ensuring it remains valid.
Create a dedicated email-and-password guest account and invite it to the feeder you want Inky to follow. Bird Buddy documents the guest invitation flow. This is also the supported path when the feeder owner signs in with Apple, Google, or Facebook. Keep the guest account for this integration rather than using it interactively, so postcard state stays predictable.
Add the provider without putting credentials in TOML:
[discovery]
sources = ["inaturalist", "birdbuddy"]
birdbuddy_include_manual_sightings = false
zip_code = "12345"
radius_km = 8
species_limit = 50
window = "last-30-days"Then authenticate once. The interactive command shows the authorization attestation before asking for credentials:
inky-bird-frame birdbuddy login --config /path/to/config.toml
inky-bird-frame birdbuddy status --config /path/to/config.tomlFor noninteractive secret-manager use, inject INKY_BIRDBUDDY_EMAIL and
INKY_BIRDBUDDY_PASSWORD for that command only and add
--confirm-authorized-access. Never put the password on the command line or in
config.toml. If the account can see more than one feeder, login reports the
available names and IDs without saving authentication; rerun with the intended
--feeder-id.
Login stores only the attestation time, selected feeder, and rotating refresh
token in mode-0600 controller state. Access tokens remain in memory. The
password and email are never retained. birdbuddy logout --config /path/to/config.toml --yes removes local authentication while preserving
detection history; it does not claim to revoke the remote token.
Bird Buddy rotates its refresh token once before each authenticated sync, so
even discover and seed previews must atomically update authentication state.
Preview commands do not commit detection history or taxonomy cache changes.
The provider reads high-confidence species metadata and media identifiers from new postcards. It also reads the species, capture time, origin, feeder ID, and media identifier from confirmed history so a postcard is not missed when someone confirms it before Inky's next poll. The identifiers link a confirmed correction back to its cached postcard; they never enter the public catalog. Inky never requests media URLs or downloads photos, video, or audio. It does not convert, reanalyze, collect, edit, discard, or share a postcard or control a feeder. The independent pybirdbuddy project demonstrates the private API's current password and guest-account behavior but is not an authoritative Bird Buddy API contract and is not a runtime dependency here.
Bird Buddy removes a postcard from the new-postcard feed after it is confirmed. The controller therefore combines exact postcard history with confirmed metadata. Exact postcards are deduplicated and counted once per species. When confirmed history covers every media item on a cached postcard, its species replace the earlier preview classification. An incomplete match leaves the cached postcard unchanged rather than guessing. Confirmed records fill gaps in species presence and supply the newest capture time, but they do not increase an existing postcard count: one postcard can contain several media records, and treating each as a visit would inflate the result. Counts that rely only on confirmed history are conservative lower bounds.
Manually added app sightings use Bird Buddy's account-level CUSTOM_ID origin.
They are not tied to the selected feeder and may have been recorded somewhere
else, so Inky excludes them by default. Include them only when that matches how
you use the account:
[discovery]
sources = ["birdbuddy"]
birdbuddy_include_manual_sightings = trueThe choice is reversible. Manual evidence remains in the private history so it
does not need to be fetched again, but it stops affecting discovery as soon as
the option is disabled. Livestream WATCHING records and postcard history from
other feeders are always excluded.
Exact postcard events remain in private history for 366 days, with older
all-time totals retained separately. A compact correction ledger keeps media
linkage for an archived postcard only while Bird Buddy still exposes every
linked media record; it can adjust the totals without retaining the postcard
ID. Confirmed history keeps the newest evidence per species and provenance.
The first sync can import only metadata Bird Buddy still exposes. all-time
combines exact history accumulated since the integration began with
conservative presence evidence from the current confirmed collection; it is
not a claim about Bird Buddy's total visit count. Repeated polls are idempotent,
and changed or removed classifications replace prior species while Bird Buddy
still exposes the underlying record.
History written before media linkage cannot prove which confirmed record belongs to a cached postcard. The first successful sync after this upgrade drops those unlinked, short-lived postcard rows instead of risking a stale classification. Current confirmed metadata can still restore conservative species presence.
Inspect the saved authentication state and selected feeder with birdbuddy status, then run discover to verify live access and inspect the birdbuddy
provider. The status command is local-only; discover performs the authenticated
refresh and rotates the saved token. Multiple accessible feeders require a new
login with --feeder-id. An invalid or revoked session requires an explicit
login; schema or pagination errors may mean the private API changed.
Inky redacts credentials, tokens, email addresses, postcard identifiers, and
raw server payloads from errors. Feeder names and IDs are intentionally shown
when needed to choose or verify a feeder. Remove birdbuddy from sources to
disable synchronization while retaining auth and history, or run birdbuddy logout --yes to remove local authentication while preserving history.
eBird returns recent public sightings and an eBird species code. The controller searches iNaturalist for the exact active species-rank scientific name and uses the resulting iNaturalist taxon ID as the canonical catalog identity. Ambiguous, inactive, hybrid, subspecies, domestic, and unmatched records are deferred. A seven-day negative cache prevents one mismatch from generating requests every 15 minutes.
iNaturalist remains the source for taxonomy and research-grade CC0/CC-BY reference photographs. eBird and Macaulay Library media are not copied into the generation pipeline.
Each provider receives the configured species_limit, so the merged result may
be larger before duplicate species are removed. If one provider fails, the
others still refresh the active catalog and the controller records which source
failed. A refresh fails only when every configured provider fails. In that
case, the prior active catalog stays in place.
The legacy singular values source = "inaturalist", "ebird", "combined",
and "birdweather" remain accepted. Do not set both source and sources.
Version 0.5.0 removed source = "all" because its frozen three-provider meaning
became misleading as new providers were added. Replace it without changing
behavior:
[discovery]
sources = ["inaturalist", "ebird", "birdweather"]Configuration loading now fails with this migration when source = "all" is
present. New configurations should always use the explicit array.
iNaturalist supplies observation counts. eBird Archive counts checklists that
contain each species. BirdWeather and BirdNET-Go supply station detection
counts, BirdNET Analyzer counts imported detection records, and Bird Buddy
supplies distinct postcard counts. eBird's nearby endpoint supplies presence
rather than a comparable aggregate count, so eBird-only species receive weight
one. When several providers resolve to the same species, Inky keeps the largest
provider count instead of adding unlike measurements together. These counts
describe different collection methods and should not be compared as equivalent
evidence. shuffle_bag is the recommended source-neutral rotation policy.
BirdWeather, BirdNET-Go, and Bird Buddy also supply each species' latest
detection timestamp. By default, a display node shows the newest detection once
before returning to its configured rotation. This is display priority, not a
confidence claim or a live event stream: the timestamp advances only when the
controller refreshes a provider. Configure
display_node.prioritize_latest_detection = false to disable it. The discover
command includes latest_detection_at on species entries when a configured
provider supplies that timestamp and omits the field otherwise.
The eBird nearby API supports at most 30 days and 50 km. An eBird personal archive is bounded to 512 MiB of uncompressed CSV data per import. BirdWeather returns at most 100 species per station-species request. Bird Buddy can return no more history than remains in its current feed on first setup, then uses private local history. Use an explicit iNaturalist seed for longer pre-existing periods:
inky-bird-frame seed --config /path/to/config.toml \
--source inaturalist --window last-year --species-limit 500For an exact historical place-and-time window, provide both coordinates and both inclusive ISO dates. This command-scoped location does not alter the configured discovery location:
inky-bird-frame seed --config /path/to/config.toml \
--source inaturalist --latitude 40.7128 --longitude -74.0060 \
--radius-km 11 --start-date 2026-04-01 --end-date 2026-04-03 \
--species-limit 500 --dry-runCoordinate-radius historical ranges require iNaturalist because the other
configured providers cannot guarantee the same place-and-time query semantics.
An imported eBird Archive supports personal-checklist date ranges without a
location filter. Run without --dry-run only after reviewing the structured
result. The seed stores every distinct taxon as private collection membership
and queues only missing, non-terminal plates. It stores no raw observation
records or command-scoped coordinates. Approved seeded taxa become active
immediately; unapproved taxa become active after a plate passes review and
approval.
Collection membership is independent from catalog synchronization. A plate
added to the public catalog later is not added to the private collection merely
because it was downloaded. Use collection import-approved for an explicit
point-in-time migration or collection add TAXON_ID for one taxon. Current
observations remain an independent source of active eligibility.
Repeat --source for a multi-provider seed, for example --source inaturalist --source ebird. CLI overrides accept concrete providers rather than legacy
group aliases.
Review the official eBird API documentation and eBird data-use guidance and the official BirdWeather V1 API before commercial use. BirdWeather is a hosted dependency and its availability, retention, accepted detection format, and API behavior remain outside this project's control. Bird Buddy is likewise a hosted private dependency whose schema, availability, and access policy remain outside this project's control. The private discovery snapshot may contain provider diagnostics and source names, but tokens, feeder/postcard identifiers, location details, and observation details never enter reusable plates or the public catalog.