Skip to contents

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.

library(taxify)

spp <- c("Quercus robur", "Pinus sylvestris", "Bellis perennis", "Morus alba")

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 DOI

Taken 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 citations

accepted_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 names

This 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 5361889

Four 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    TRUE

is_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 5361889

For 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:

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] 200

The 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 5361889

A 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