Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/zotero/zotero-connectors/llms.txt

Use this file to discover all available pages before exploring further.

When the Zotero desktop application is not running — or is otherwise unreachable — the connector falls back to saving bibliographic items directly to the user’s zotero.org library via the Zotero Web API v3. The Zotero.API singleton (src/common/api.js) implements an OAuth 1.0a authorisation flow using the OAuthSimple library (src/common/oauthsimple.js), then uses the obtained API key to create items and upload file attachments.

When the Web API is Used

ItemSaver.saveItems() calls _saveToZotero() first. If that call throws with status === 0 (the connector server is offline), it falls back to _saveToServer(), which uses Zotero.API:
saveItems: async function(items, attachmentCallback, itemsDoneCallback=()=>0) {
  try {
    return await this._saveToZotero(items, attachmentCallback, itemsDoneCallback);
  }
  catch (e) {
    if (e.status == 0) {
      return this._saveToServer(items, attachmentCallback, itemsDoneCallback);
    }
    throw e;
  }
}
Access to the zotero.org Web API requires a Zotero account. Users who have not yet authenticated are prompted to log in during their first save attempt when the desktop client is offline.

OAuth 1.0a Configuration

All OAuth endpoints and client credentials are defined in ZOTERO_CONFIG:
const ZOTERO_CONFIG = {
  API_URL: 'https://api.zotero.org/',
  OAUTH: {
    ZOTERO: {
      REQUEST_URL:  'https://www.zotero.org/oauth/request',
      ACCESS_URL:   'https://www.zotero.org/oauth/access',
      AUTHORIZE_URL:'https://www.zotero.org/oauth/authorize',
      CALLBACK_URL: 'https://www.zotero.org/connector_auth_complete',
      CLIENT_KEY:   '05a4e25d3d9af8922eb9',
      CLIENT_SECRET:'8dda1d6aa188bdd3126e'
    }
  }
}
OAuth signatures are computed by OAuthSimple (src/common/oauthsimple.js), which implements the standard HMAC-SHA1 signature method.

OAuth Flow: Zotero.API.authorize()

authorize() implements the full three-legged OAuth 1.0a dance:
1

Request temporary token

A signed POST is sent to OAUTH.ZOTERO.REQUEST_URL using the client key and secret. The response contains an oauth_token and oauth_token_secret (the temporary credentials).
oauthSimple.setURL(config.OAUTH.ZOTERO.REQUEST_URL);
oauthSimple.setAction("POST");
let xmlhttp = await Zotero.HTTP.request("POST", config.OAUTH.ZOTERO.REQUEST_URL, {
  body: '',
  headers: {"Authorization": oauthSimple.getHeaderString()}
});
var data = _decodeFormData(xmlhttp.responseText);
_tokenSecret = data.oauth_token_secret;   // stored in closure
2

Open authorisation window

The user is redirected to the Zotero authorisation page. The URL is built from AUTHORIZE_URL with the signed token and scopes:
var url = signature.signed_url
  + "&library_access=1"
  + "&notes_access=0"
  + "&write_access=1"
  + "&name=Zotero Connector for Chrome";  // or Firefox / Safari / Edge
A browser window (900 × 600) is opened and the promise returned by authorize() is left pending.
3

Handle the callback: onAuthorizationComplete()

When the user approves access, zotero.org redirects to CALLBACK_URL. The connector intercepts the redirect and calls Zotero.API.onAuthorizationComplete(data, tab).The callback:
  1. Exchanges the temporary token for permanent access credentials via ACCESS_URL.
  2. Verifies the key has user.library and user.write permissions by calling GET /users/<id>/keys/current with header Zotero-API-Key.
  3. Stores credentials in preferences: auth-token, auth-token_secret, auth-userID, auth-username.
Zotero.Prefs.set('auth-token',        data.oauth_token);
Zotero.Prefs.set('auth-token_secret', data.oauth_token_secret);
Zotero.Prefs.set('auth-userID',       data.userID);
Zotero.Prefs.set('auth-username',     data.username);
authorize() is re-entrant-safe: if an authorisation window is already open when a second call is made, the existing window is brought to the front and the same promise is returned rather than opening a duplicate.

User Info: Zotero.API.getUserInfo()

Returns stored OAuth credentials as an object, or null if the user has not yet authorised:
this.getUserInfo = async function() {
  let keys = ['auth-token_secret', 'auth-userID', 'auth-username'];
  return Zotero.Prefs.getAsync(keys).catch(function() {
    return null;
  });
}
The returned object has keys auth-token_secret, auth-userID, and auth-username.

Creating Items: Zotero.API.createItem(payload, askForAuth?)

Posts items to the user’s library using the stored API key:
var url = config.API_URL + "users/" + userInfo['auth-userID'] + "/items";
var options = {
  body: JSON.stringify(payload),
  headers: {
    "Content-Type":    "application/json",
    "Zotero-API-Key":  userInfo['auth-token_secret'],
    "Zotero-API-Version": "3"
  }
};
var xhr = await Zotero.HTTP.request("POST", url, options);
If the server responds with HTTP 403 (API key revoked) and askForAuth is not false, createItem() automatically clears stored credentials and restarts the OAuth flow.
payload
object[]
required
An array of item objects in the Zotero API JSON format, produced by Zotero.Utilities.Item.itemToAPIJSON().
askForAuth
boolean
When false, throws immediately if not authorised instead of starting the OAuth flow. Used in retry chains to prevent infinite loops.

Uploading Attachments: Zotero.API.uploadAttachment(attachment)

File upload uses the Zotero Web API’s two-phase upload protocol:
1

Authorise upload

A POST to /users/<id>/items/<key>/file with If-None-Match: * and URL-encoded file metadata requests an upload authorisation. If the server responds { "exists": 1 }, the file is already uploaded and no further action is required.
var data = {
  "md5":         attachment.md5,
  "filename":    attachment.filename,
  "filesize":    attachment.data.byteLength,
  "mtime":       (+new Date),
  "contentType": attachment.mimeType
};
2

Upload binary data

The authorisation response provides prefix, suffix, url, contentType, and uploadKey. The attachment data is wrapped:
var uploadData = new Uint8Array(
  attachment.data.byteLength + prefixLength + suffixLength
);
Zotero.Utilities.stringToUTF8Array(response.prefix, uploadData, 0);
uploadData.set(new Uint8Array(attachment.data), prefixLength);
Zotero.Utilities.stringToUTF8Array(response.suffix, uploadData,
  attachment.data.byteLength + prefixLength);

await Zotero.HTTP.request("POST", response.url, {
  headers: {
    "Zotero-API-Key": userInfo['auth-token_secret'],
    "Zotero-API-Version": "3",
    "Content-Type": response.contentType
  },
  body: uploadData.buffer
});
3

Register upload

A final POST to the file endpoint with body: "upload=<uploadKey>" and If-None-Match: * tells the server the upload is complete.
The required properties for uploadAttachment are data (a typed array / ArrayBuffer), key (item key — alphanumeric only), md5, and mimeType. The md5 hash is computed by Zotero.ItemSaver.md5() using an embedded implementation derived from pdf.js.

Clearing Credentials: Zotero.API.clearCredentials()

Removes all stored OAuth tokens from preferences:
this.clearCredentials = function() {
  let keys = ['auth-token', 'auth-token_secret', 'auth-userID', 'auth-username'];
  Zotero.Prefs.clear(keys);
}
This is equivalent to logging out. The user will be prompted to re-authorise on the next save attempt.

Credential Storage Summary

Preference KeyContent
auth-tokenOAuth access token (public)
auth-token_secretOAuth token secret / API key (used for all API calls)
auth-userIDNumeric Zotero user ID
auth-usernameZotero username (displayed in the connector UI)

Further Reading

The Zotero Web API v3 documentation — including item schemas, collection endpoints, file upload details, and rate limits — is available at https://www.zotero.org/support/dev/web_api/v3/start.
All API requests include the Zotero-API-Version: 3 header. Rate limits are enforced server-side; the connector does not currently implement client-side backoff for 429 responses when saving via the Web API.

Build docs developers (and LLMs) love