You have a list of species names and you want their occurrence records from GBIF, the Global Biodiversity Information Facility. GBIF does not serve records by name. It serves them by taxon key, an integer identifying one taxon in the GBIF backbone, so the list has to be resolved to keys before anything can be requested.
taxify() resolves the names to keys.
gbif_request() sends those keys to GBIF, which prepares the
records as a download on its side and issues a DOI for it.
gbif_fetch() waits for that download to finish and returns
the records with your names attached. The request and the fetch are
separate because GBIF prepares a download in the background, which can
take a while.
GBIF caps a download query at 12,000 characters, which fits about
1,187 taxon keys, and a checklist of a few hundred names can pass that
once its homonyms are counted. gbif_request() splits a
longer list into downloads of up to 1,000 keys and queues them within
GBIF’s limit of three concurrent downloads, so a list of any length is
still one call. gbif_fetch() waits for all of them and
stacks the records, and every download gets its own DOI (details).
Matching the names and reading off their keys runs offline. Sending
the request and fetching the download both need rgbif and
a free GBIF account set up in .Renviron — see Setting up rgbif.
Match first, then request
The whole run, from a list of names to a DOI you can cite:
matched <- taxify(spp, backbone = "gbif") # resolve the names to GBIF keys
dl <- gbif_request(matched) # ask GBIF for every key
recs <- gbif_fetch(dl) # wait, download, attach your names
cite(recs) # the download's DOITaken a step at a time. taxify() resolves the list
against the local GBIF backbone and returns an ordinary data frame, so
you can see what every name landed on before anything is sent:
matched <- taxify(spp, backbone = "gbif")
matched[, c("input_name", "accepted_name", "accepted_id", "taxonomic_status", "n_ids")]
#> input_name accepted_name accepted_id taxonomic_status n_ids
#> 1 Quercus robur Quercus robur 2878688 ACCEPTED 3
#> 2 Pinus sylvestris Pinus sylvestris 5285637 ACCEPTED 6
#> 3 Bellis perennis Bellis perennis 3117424 ACCEPTED 1
#> 4 Morus alba Morus alba 5361889 ACCEPTED 1
#> Sources: GBIF 2023.08 | cite() for full citationsaccepted_id is the key taxify() reported;
n_ids above 1 means GBIF files that name under several
keys, and taxify_ids() lists them one per row.
gbif_request() is the step that contacts GBIF. It takes
the matched result, collects every one of those keys and submits them as
one download through rgbif, after which
GBIF prepares the records in the background. gbif_fetch()
waits for the download and returns the records with your queried name on
every row:
dl <- gbif_request(matched)
recs <- gbif_fetch(dl)The records arrive with an input_name column carrying
the name you queried, so they can be counted or grouped by species
straight away. cite(recs) closes the run: it reports the
download’s DOI beside the backbone the names were matched against, which
is the pair GBIF asks you to cite. Citing
the download shows what it prints.
Or match and request in one call
gbif_request() also takes the names themselves and
matches them against GBIF on the way past, with matching arguments
(fuzzy, kingdom, region) passed
through to taxify():
dl <- gbif_request(spp) # match, then ask GBIF for every key
recs <- gbif_fetch(dl) # wait, download, import, attach your namesThis is the same run with the match folded into the request, and
cite(recs) closes it the same way. Matching first is only
needed when you want to read the matches before anything is sent. The
matched table is still there afterwards, as
attr(dl, "taxa").
dry_run = TRUE sits between the two: it matches, reports
the keys, and contacts nothing.
gbif_request(spp, dry_run = TRUE)
#> 11 GBIF key(s) from 4 matched name(s). About 4,090,085 occurrence record(s) across them.
#> [1] 2878688 7911626 7586523 5285637 7718215 8116613 9079676 8251433 7393648
#> [10] 3117424 5361889Four names, eleven keys. The rest of this article is where the other seven come from, and what to check before sending a request for four million records.
Why one name sometimes gives several keys
GBIF files a name under more than one key in two situations: a
homonym, where two authors published the same name for different taxa,
and a name held once as an accepted record and again as a doubtful or
duplicate one. taxify_ids() lays those out, one row per
name and key, with the occurrence count each key carries:
taxify(spp, backbone = "gbif") |>
taxify_ids()
#> input_name accepted_id accepted_name taxonomic_status n_occurrences is_pick
#> 1 Quercus robur 2878688 Quercus robur ACCEPTED 1725528 TRUE
#> 2 Quercus robur 7911626 Quercus robur DOUBTFUL 2 FALSE
#> 3 Quercus robur 7586523 Quercus robur DOUBTFUL 9 FALSE
#> 4 Pinus sylvestris 5285637 Pinus sylvestris ACCEPTED 1204947 TRUE
#> 5 Pinus sylvestris 7718215 Pinus sylvestris DOUBTFUL 0 FALSE
#> 6 Pinus sylvestris 8116613 Pinus sylvestris DOUBTFUL 0 FALSE
#> 7 Pinus sylvestris 9079676 Pinus sylvestris DOUBTFUL 0 FALSE
#> 8 Pinus sylvestris 8251433 Pinus sylvestris DOUBTFUL 0 FALSE
#> 9 Pinus sylvestris 7393648 Pinus sylvestris DOUBTFUL 0 FALSE
#> 10 Bellis perennis 3117424 Bellis perennis ACCEPTED 1107989 TRUE
#> 11 Morus alba 5361889 Morus alba ACCEPTED 51610 TRUEis_pick marks the key taxify() reported in
accepted_id. The rest are the other records of the same
name. Requesting only the pick would have missed the eleven records
sitting under the two doubtful Quercus robur keys.
n_occurrences is the count taken when the backbone was
built. For an accepted key it includes the records of that taxon’s
synonyms and descendants, which is what a download by that key returns,
so it is a good guide to the size of a request and not an exact
promise.
Choosing what to request
Every key is requested by default. strict = TRUE narrows
the request to the single key taxify() reported:
gbif_request(spp, strict = TRUE, dry_run = TRUE)
#> 4 GBIF key(s) from 4 matched name(s). About 4,090,074 occurrence record(s) across them.
#> [1] 2878688 5285637 3117424 5361889For this list the two choices differ by 11 records out of 4.09
million, so asking for every key costs almost nothing and picks up
records that would otherwise be dropped. For a list with many homonyms
the difference is larger, and taxify_ids() is where you see
it before choosing.
Setting up rgbif
Sending the request is the part that needs rgbif:
install.packages("rgbif")method = "search" works with nothing further.
method = "download" is an authenticated call and needs a
GBIF account, which is free: register at gbif.org.
rgbif reads that account from three environment variables. Its own
documentation recommends keeping them in .Renviron rather
than passing them as arguments, which also keeps them out of your
scripts:
usethis::edit_r_environ()Add the three lines, with no quotes and no spaces around the
=:
GBIF_USER=yourname
GBIF_PWD=yourpassword
GBIF_EMAIL=you@example.org
Save, then restart R so the file is read. rgbif also accepts the
lower-case names gbif_user, gbif_pwd and
gbif_email in .Rprofile if you prefer that;
see ?rgbif::occ_download.
To confirm GBIF accepts the account, ask it for your own past downloads. That call is authenticated, so it only succeeds when the credentials are right:
rgbif::occ_download_list(limit = 5)If a variable is missing, gbif_request() stops before
contacting GBIF and names which one. If the password is wrong, GBIF
answers 401.
Sending the request
method = "search" sends unauthenticated searches and
returns the records directly. It needs no account, and the GBIF search
API limits how deep it will page, so it suits a look at a few taxa:
recs <- gbif_request(spp, method = "search", strict = TRUE, limit = 50)
nrow(recs)
#> [1] 200The records come back as one data frame with a
taxon_key_requested column, so a row can be traced back to
the key that asked for it.
method = "download" is the default. It submits an
asynchronous GBIF download, which has no record cap and is issued a DOI
you can cite. It needs a GBIF account:
dl <- gbif_request(spp, method = "download")
recs <- gbif_fetch(dl)gbif_fetch() waits for the download, retrieves it,
imports it, and attaches your queried names.
All of the keys go into one download: they are sent as a single
taxonKey IN (...) predicate, so the whole list comes back
as one dataset under one DOI. GBIF prepares a download of four million
records on its own servers, which takes a while, so the call returns as
soon as the request is queued.
Long lists are split across downloads
GBIF caps a download query at 12,000 characters. A taxonKey predicate costs about 10 characters per key, so roughly 1,187 keys fit, and a checklist of a few hundred names can pass that once its homonyms are counted.
Above the limit gbif_request() splits the keys into
chunks of 1,000 and submits them through rgbif’s
occ_download_queue(), which respects GBIF’s rule of three
concurrent downloads per user:
dl <- gbif_request(long_species_list, method = "download")
#> 2431 keys exceed GBIF's query limit; splitting into 3 downloads of up to
#> 1000 keys. Each gets its own DOI.dl is then a vector of download keys rather than one,
and gbif_fetch() waits for all of them and stacks the
records, so fetching a split request looks the same as fetching a single
one:
recs <- gbif_fetch(dl)Only the query naming the keys is capped. GBIF prepares downloads of tens of millions of records server-side, however many that comes to.
Linking the records back to your names
The records come back keyed by GBIF taxon, not by the name you asked
for, so they have to be traced back before you can count or group by
species. gbif_backmatch() does that, adding
requested_key, input_name and
accepted_name:
recs <- gbif_request(spp, method = "search", limit = 100)
recs <- gbif_backmatch(recs, recs)
table(recs$input_name, useNA = "ifany")gbif_fetch() calls it for you, so this is only needed
when you fetched the records some other way or asked it not to:
recs <- gbif_fetch(dl, backmatch = FALSE)
recs <- gbif_backmatch(recs, dl)It takes the object gbif_request() returned, which
carries the key table as an attribute. A taxify_ids() table
or a taxify() result works too, so records imported by hand
can still be linked back:
matched <- taxify(spp, backbone = "gbif")
recs <- gbif_backmatch(recs, matched)Why not just join on taxonKey
GBIF returns the records of a key’s descendants along with its own,
and a record identified below species level carries its own key.
Requesting Pinus nigra (5284809) returns records whose
taxonKey is 5686674, P. nigra subsp.
salzmannii; those rows carry the requested key in
speciesKey and nowhere else.
In a 300-record sample of that request, 50 rows are identified to a
subspecies. Joining on taxonKey matches 250 of 300.
gbif_backmatch() matches all 300, because it tries the
record’s keys from the most specific outwards and takes the first that
is one of the keys you requested.
Rows matching no requested key keep NA in the three
added columns and are counted in a message rather than dropped.
Citing the download
A GBIF download gets a DOI once it is ready, and citing it is what
lets someone else retrieve the same records. The download key travels
with the records, so cite() reports the DOI next to the
backbone the names were matched against:
recs <- gbif_fetch(dl)
cite(recs)
#> ── taxify citations ────────────────────────────────────────────────
#> [1] Colling G (2026). taxify: Offline Taxonomic Name Matching (version 0.6.0).
#> [2] GBIF Secretariat (2024). GBIF Backbone Taxonomy. doi:10.15468/39omei
#> [3] GBIF.org (2026-09-23) GBIF Occurrence Download https://doi.org/10.15468/dl.ckzs32
#> ────────────────────────────────────────────────────────────The DOI is read from GBIF when you call cite(), not
stored when the request was made, because GBIF issues it only once the
download has finished preparing. A download still running is reported as
having no DOI yet.
cite(recs, file = "refs.bib") writes the same entries as
BibTeX, the download included.
Results matched against another backbone
Every match above named backbone = "gbif". taxify’s
default chain starts at COL XR, so a plain taxify(spp)
resolves most names against something other than GBIF and the result
carries no GBIF keys. Passing one of those is fine: the rows without a
GBIF key are re-matched against GBIF, with a message, because GBIF only
accepts its own keys.
taxify(spp) |>
gbif_request(dry_run = TRUE)
#> No rows matched by GBIF; re-matching the names against the GBIF backbone, whose keys a GBIF request needs.
#> 11 GBIF key(s) from 4 matched name(s). About 4,090,085 occurrence record(s) across them.
#> [1] 2878688 7911626 7586523 5285637 7718215 8116613 9079676 8251433 7393648
#> [10] 3117424 5361889A mixed result, where the one name only GBIF carries landed on
gbif and the rest elsewhere, is handled the same way: the
rows without a key are re-matched and the rest are kept, so every name
in the list reaches the request.
Matching arguments go in the taxify() call here, not in
gbif_request(), which takes them only when it is doing the
matching itself:
taxify(spp, backbone = "gbif", kingdom = "Plantae") |>
gbif_request()See also
taxify_ids()for the keys and their occurrence counts on their own.vignette("large-scale")for matching lists too large to hold in memory.vignette("quickstart")for the matching itself.