Skip to content
ProductOfAmericaPublic

About

Unofficial TypeScript SDK for John Deere Operations Center API

Topics

Resources

Contributing

Security policy

Stars

13 stars

Watchers

1 watching

Forks

Repository files navigation

deere-sdk

deere-sdk

Unofficial TypeScript SDK for John Deere Operations Center API

npm version npm downloads license TypeScript Tests Coverage API Health OpenSSF Scorecard OpenSSF Best Practices

Installation • Quick Start • API Reference • API Status


Highlights

  • 28 APIs with 203 methods — Full coverage of John Deere agricultural APIs
  • Fully typed — Auto-generated TypeScript types from OpenAPI specs
  • Auto-pagination — listAll() methods handle pagination automatically
  • HAL support — Built-in link following for John Deere's HAL-style responses
  • Automatic retries — Exponential backoff with jitter for transient failures
  • Daily health checks — Automated monitoring of spec availability and live route reachability

Installation

npm install deere-sdk
pnpm add deere-sdk
yarn add deere-sdk

Quick Start

import {Deere} from 'deere-sdk';

const deere = new Deere({
    accessToken: 'your-oauth-access-token',
    environment: 'sandboxapi', // or 'api' for production
});

// List all organizations
const orgs = await deere.organizations.listAll();
const orgId = orgs[0]?.id;
if (!orgId) throw new Error('No organization available for this token');

// Get fields for an organization
const fields = await deere.fields.listAll(orgId);

// Get farms for an organization
const farms = await deere.farms.listAll(orgId);

Authentication

This SDK requires an OAuth 2.0 access token from John Deere:

  1. Register at developer.deere.com
  2. Create an application and get your client ID/secret
  3. Implement the OAuth 2.0 authorization code flow
  4. Use the access token in the SDK
OAuth Scopes

Descriptions are quoted verbatim from John Deere's developer documentation. OpenID Connect scopes (openid, profile, email, address, phone, device_sso) are omitted — this table covers only the API-access scopes.

Scope User/Connection Permission Description
ag1 Locations Access Level 1 View Locations (Clients, Farms, Fields and Associated Data)
ag2 Locations Access Level 1
Locations Access Level 2
View Locations (Clients, Farms, Fields and Associated Data)
Analyze Production Data (Website Access Only)
ag3 Locations Access Level 1
Locations Access Level 2
Locations Access Level 3
View Locations (Clients, Farms, Fields and Associated Data)
Analyze Production Data (Website Access Only)
Manage Locations & Production Data (Website and API Access)
eq1 Equipment Access Level 1
RDA
Setup & WDT
View Equipment
Remote Display Access
Setup File Creator, Products, and Wireless Data Transfer
eq2 Equipment Access Level 1
Equipment Access Level 2
Equipment Access Level 3
RDA
Setup & WDT
View Equipment
Edit Equipment (also View Detailed Machine Measurements)
Manage Equipment
Remote Display Access
Setup File Creator, Products, and Wireless Data Transfer
org1 Organization Management Access Level 1 View Staff, Operators, and Partners
org2 Organization Management Access Level 1
Organization Management Access Level 2
View Staff, Operators, and Partners
Modify Staff, Operators, and Partners
work1 Work and Crop Plans Access Level 1 View Work and Crop Plans
work2 Work and Crop Plans Access Level 1
Work and Crop Plans Access Level 2
View Work and Crop Plans
View Work and Crop Plans †
finance1 Financial Access Level 1 View Financials
finance2 Financial Access Level 1
Financial Access Level 2
View Financials
Manage Financials
files Files API Access
Equipment Access Level 3
Setup & WDT
Files API Access (ag3 scope also required for most file types)
Manage Equipment
Setup File Creator, Products, and Wireless Data Transfer
offline_access API Authentication Only Request a Refresh Token

† Reproduced as published by John Deere; the two Level-2 description lines are identical in the upstream docs, which appears to be a documentation typo.

Authoritative list of scopes supported by the authorization server: https://signin.johndeere.com/oauth2/aus78tnlaysMraFhC1t7/.well-known/oauth-authorization-server.

Environments

The SDK uses raw John Deere subdomain names as environment values. Pass the subdomain (without .deere.com) as environment. Default: sandboxapi.

Environment URL Use Case
api api.deere.com Live production
sandboxapi sandboxapi.deere.com Development (default)
partnerapi partnerapi.deere.com Partner production
apicert apicert.deere.com Production certification
partnerapicert partnerapicert.deere.com Partner certification
apiqa.tal apiqa.tal.deere.com QA tier
partnerapiqa partnerapiqa.deere.com Partner QA
sandboxapiqa sandboxapiqa.deere.com Sandbox QA
apidev.tal apidev.tal.deere.com Internal dev tier (rarely used)

Not every API is served in every environment, and what the SDK does about it depends on how John Deere hosts the API:

  • A few APIs are served from fixed hosts of their own. A call in an environment such an API has no host for throws UnsupportedEnvironmentError before any request goes out, naming the environments that API does support. With hateoas: true some of those calls behave differently; see Strict Mode.
  • Most are served from https://{environment}.deere.com/.... For those the SDK builds the URL for whichever environment you configured and sends the request, including when John Deere's spec for that API does not list the environment. Nothing is checked first, and whether the request is answered is up to John Deere.

The constructor only rejects an environment name that isn't in the table above.

Migrating from v1

v1 accepted friendly environment names. v2.0.0 replaces them with the raw John Deere subdomain names so URL routing comes directly from each spec's OpenAPI servers block — no more hardcoded hostname maps.

Passing a v1 name to the v2 constructor throws immediately with a migration hint:

v1 (pre-2.0.0) v2.0.0+
production api
sandbox sandboxapi
partner partnerapi
cert apicert
qa apiqa.tal
// v1
const deere = new Deere({ accessToken, environment: 'sandbox' });

// v2.0.0
const deere = new Deere({ accessToken, environment: 'sandboxapi' });

The SDK now uses separate host subdomains per spec family (e.g., equipmentapi.deere.com for machine telemetry, api.deere.com for organizations) on the production tier. v1 silently routed everything through api.deere.com, which worked for most flows but mis-routed some machine-data endpoints. v2 routes each call to the exact host the spec declares.

Migrating from v2

One method was removed. Everything else is source-compatible.

Removed Replacement
deere.organizations.listUsers None

GET /organizations/{orgId}/users returns 404 in every environment and appears in none of the 85 APIs John Deere publishes. It had reached the SDK through a hand-written spec injection rather than from a published spec, so it shipped as a typed method that could never succeed. Any call to it was already failing.

There is no replacement. GET /organizations/{orgId}/staff exists but returned 403 to an application holding full scopes, and no response body has been captured, so shipping a method for it would mean inventing its types. See CHANGELOG.md for the measurements.

Two methods that previously returned 404 now work, with no signature change: deere.notifications.get and deere.products.listActiveIngredients. Both were building /platform URLs for routes John Deere serves from /isg.


API Reference

Operations Center APIs

API Property Methods Description
Organizations deere.organizations 4 Organization management
Fields deere.fields 8 Field CRUD and boundaries
Farms deere.farms 8 Farm management
Boundaries deere.boundaries 8 Field boundary management
Clients deere.clients 8 Customer management
Equipment deere.equipment 16 Machines and implements
Field Operations deere.fieldOperations 6 Harvests, plantings, applications
Crop Types deere.cropTypes 5 Crop type catalog
Products deere.products 39 Seeds and chemicals catalog
Map Layers deere.mapLayers 15 Map layer management
Files deere.files 10 File management
Flags deere.flags 15 Field flags/markers
Guidance Lines deere.guidanceLines 5 GPS guidance lines
Operators deere.operators 7 Machine operator management
Users deere.users 3 User information
Assets deere.assets 9 Asset tracking
Webhooks deere.webhook 7 Event subscriptions
Connections deere.connectionManagement 4 OAuth connections

deere.organizations.listOrganizations is deprecated: John Deere now documents GET /users/{userName}/organizations in the Users API, so use deere.users.listOrganizations (same arguments and response). The deprecated method keeps working and is removed in the next major version.

Machine Data APIs

API Property Methods Description
Machine Locations deere.machineLocations 2 GPS location history
Machine Alerts deere.machineAlerts 2 DTC alerts
Engine Hours deere.machineEngineHours 2 Engine hours tracking
Hours of Operation deere.machineHoursOfOperation 2 On/off duration
Device State deere.machineDeviceStateReports 1 Terminal state reports
Notifications deere.notifications 5 Push notifications
Harvest ID deere.harvestId 3 Cotton module data
AEMP deere.aemp 1 ISO 15143-3 fleet data
Equipment Measurement deere.equipmentMeasurement 1 Third-party measurements
Partnerships deere.partnerships 7 Organization partnerships

Usage Examples

Organizations
// List all organizations
const orgs = await deere.organizations.list();
const allOrgs = await deere.organizations.listAll();

// Get a specific organization
const org = await deere.organizations.get('org-id');
Fields
// List fields in an organization
const fields = await deere.fields.list('org-id');
const allFields = await deere.fields.listAll('org-id');

// Filter fields
const filtered = await deere.fields.list('org-id', {
    farmName: 'North Farm',
    recordFilter: 'AVAILABLE'
});

// Get a specific field
const field = await deere.fields.get('org-id', 'field-id');

// Create a field
await deere.fields.create('org-id', {
    name: 'North Field',
    farmName: 'Smith Farm',
    clientName: 'John Smith'
});

// Update a field
await deere.fields.update('org-id', 'field-id', {name: 'Updated Name'});

// Delete a field
await deere.fields.delete('org-id', 'field-id');
Farms
// List farms
const farms = await deere.farms.list('org-id');
const allFarms = await deere.farms.listAll('org-id');

// Include archived
const all = await deere.farms.list('org-id', {recordFilter: 'all'});

// CRUD operations
const farm = await deere.farms.get('org-id', 'farm-id');
await deere.farms.create('org-id', {name: 'North Farm'});
await deere.farms.update('org-id', 'farm-id', {name: 'Updated'});
await deere.farms.delete('org-id', 'farm-id');

// Related resources
const clients = await deere.farms.listClients('org-id', 'farm-id');
const fields = await deere.farms.listFields('org-id', 'farm-id');
Boundaries
// List boundaries
const boundaries = await deere.boundaries.list('org-id');
const fieldBoundaries = await deere.boundaries.listBoundaries('org-id', 'field-id');

// Get specific boundary
const boundary = await deere.boundaries.getBoundaries('org-id', 'field-id', 'boundary-id');

// Generate from field operation
const generated = await deere.boundaries.get('operation-id');

// Create boundary
await deere.boundaries.create('org-id', 'field-id', {
    name: 'Main Boundary',
    active: true,
    multipolygons: [/* GeoJSON */]
});

// Update/Delete
await deere.boundaries.update('org-id', 'field-id', 'boundary-id', {name: 'New Name'});
await deere.boundaries.delete('org-id', 'field-id', 'boundary-id');
Equipment

John Deere serves the Equipment API from a single host, which this SDK treats as production: it is available on api and partnerapi only. On the default sandboxapi, equipment calls throw UnsupportedEnvironmentError before a request is sent. With hateoas: true a few of them behave differently; see Strict Mode.

import {Deere} from 'deere-sdk';

const deere = new Deere({accessToken: 'your-token', environment: 'api'});

// Get all equipment
const equipment = await deere.equipment.get();

// Filter equipment
const filtered = await deere.equipment.get({
    organizationIds: [123],
    categories: 'Machine',
    capableOf: 'Connectivity'
});

// Get equipment details
const machine = await deere.equipment.getEquipment('equipment-id');

// Reference data
const makes = await deere.equipment.list();
const types = await deere.equipment.listEquipmenttypes();
const models = await deere.equipment.listEquipmentmodels({equipmentModelName: '9RX*'});

Creating, updating and deleting equipment go to that same production host. This SDK has no test tier for them, so these calls change real data:

import {Deere} from 'deere-sdk';

const deere = new Deere({accessToken: 'your-token', environment: 'api'});

// John Deere's sample request also sends a top-level '@type': 'Machine',
// which its published schema leaves out.
await deere.equipment.create('org-id', {
    name: 'Tractor 1',
    serialNumber: 'SN-0001',
    model: {'@type': 'EquipmentModel', id: 'equipment-model-id'},
});
await deere.equipment.update('equipment-id', {name: 'Updated'});
await deere.equipment.delete('equipment-id');
Field Operations
// Prefer the safe facade when you need measurement data (yield, area,
// moisture, application rate, seeding population). It forces
// ?embed=measurementTypes on the request and guarantees the returned
// operations carry a measurementTypes array, throwing a clear error if
// JD's wire format ever drifts from the documented contract.
const ops = await deere.safe.fieldOperations.listAllWithMeasurements(
    'org-id',
    'field-id'
);
const yieldValue = ops[0].measurementTypes[0]?.averageYield?.value;

// Filter by type and season — filters forward through to the safe wrapper too.
const harvests = await deere.safe.fieldOperations.listAllWithMeasurements(
    'org-id',
    'field-id',
    { cropSeason: '2026', fieldOperationType: 'harvest' }
);

// Raw API (no forcing) — use when you only need metadata, not measurements.
const metaOnly = await deere.fieldOperations.list('org-id', 'field-id');

// Get operation details
const op = await deere.fieldOperations.get('operation-id');

// Download shapefile
const shapefile = await deere.fieldOperations.getFieldops('operation-id', {
    shapeType: 'Polygon',
    resolution: 'EachSection'
});

Why deere.safe.*? John Deere's API silently omits the measurementTypes array on fieldOperations.listAll responses unless the request passes ?embed=measurementTypes, and their OpenAPI spec doesn't document that invariant. Consumers that forget the embed param get objects with zero values for yield, area, moisture, and rate — and can't tell "missing data" from "real zero." The safe facade makes that footgun syntactically impossible: you can't call listAllWithMeasurements without the embed param because the wrapper adds it for you, and the returned type guarantees measurementTypes is present. See src/safe/ and scripts/embed-contracts.yaml for the spec-patch machinery that makes the narrowed type honest at the type level.

Machine Data
// Machine locations
const locations = await deere.machineLocations.get('principal-id', {
    startDate: '2026-01-01T00:00:00Z',
    endDate: '2026-01-31T23:59:59Z'
});

// Machine alerts
const alerts = await deere.machineAlerts.list('principal-id');

// Engine hours
const hours = await deere.machineEngineHours.list('principal-id', {lastKnown: true});

// Hours of operation
const opHours = await deere.machineHoursOfOperation.list('principal-id');

// Device state reports
const state = await deere.machineDeviceStateReports.get('principal-id');
Notifications
// List notifications
const notifications = await deere.notifications.list('org-id');

// Filter by severity
const critical = await deere.notifications.list('org-id', {
    severities: 'HIGH,CRITICAL'
});

// Create notification. John Deere's sample request also sends `links` to a
// contribution definition, which its published schema leaves out.
await deere.notifications.create({
    title: 'Action Required',
    text: 'Please review the prescription map',
    severity: 'HIGH',
    eventType: 'AGRONOMY',
});

// Delete notification
await deere.notifications.delete('source-event-id');
Assets
// List assets
const assets = await deere.assets.listAll('org-id');

// Get asset
const asset = await deere.assets.get('asset-id');

// Create asset
await deere.assets.create('org-id', {
    title: 'Fuel Tank #1',
    assetCategory: 'DEVICE',
    assetType: 'SENSOR'
});

// Asset locations
const locations = await deere.assets.listLocations('asset-id', {
    startDate: '2026-01-01T00:00:00Z',
    endDate: '2026-01-31T23:59:59Z'
});

// John Deere takes the GeoJSON as a string
await deere.assets.createLocations('asset-id', {
    timestamp: '2026-01-15T12:00:00Z',
    geometry: JSON.stringify({
        type: 'Feature',
        geometry: {
            type: 'GeometryCollection',
            geometries: [{type: 'Point', coordinates: [-93.5, 42.5]}],
        },
    }),
});
Webhooks
// List subscriptions
const subs = await deere.webhook.listAll();

// Create subscription
await deere.webhook.create({
    clientKey: 'your-client-key',
    eventTypeId: 'equipment-status',
    callbackUrl: 'https://your-server.com/webhook'
});

// Update subscription
await deere.webhook.update('subscription-id', {
    callbackUrl: 'https://new-server.com/webhook'
});
Partnerships
// List partnerships
const partnerships = await deere.partnerships.listAll();

// Create partnership request
await deere.partnerships.create({
    toOrganizationId: 'partner-org-id',
    message: 'Request to share data'
});

// Get/delete partnership
const partnership = await deere.partnerships.get('token');
await deere.partnerships.delete('token');

// Permissions
const perms = await deere.partnerships.listPermissions('token');
await deere.partnerships.createPermissions('token', {
    permissionType: 'ViewData',
    enabled: true
});

Low-Level Client

For custom endpoints or advanced use cases:

import {DeereClient, type PaginatedResponse} from 'deere-sdk';

const client = new DeereClient({
    accessToken: 'your-token',
    environment: 'sandboxapi',
    timeout: 30000,   // Per-attempt timeout in ms (default: 30000)
    maxRetries: 3,    // Retry attempts (default: 3, set to 0 to disable)
});

// Raw requests. The first argument is the spec name; it selects the host,
// since different John Deere APIs live on different subdomains and paths.
type Organization = {id?: string; name?: string};
const response = await client.get<PaginatedResponse<Organization>>('organizations', '/organizations');
const created = await client.post('fields', '/organizations/123/fields', {name: 'North'});

// Follow HAL links
const next = response.links?.find((link) => link.rel === 'nextPage');
const nextPage = next ? await client.followLink<PaginatedResponse<Organization>>(next) : undefined;

// Automatic pagination
for await (const items of client.paginate('fields', '/organizations/123/fields')) {
    console.log(items);
}

// Or collect every page at once
const all = await client.getAll('fields', '/organizations/123/fields');

The generated methods (deere.fields.get(orgId, fieldId)) percent-encode every path parameter, so an id is always exactly one segment of the path, whatever characters it contains, or the call is refused before any request goes out. The low-level client sends the path you give it as written. If you build a path from a value you did not choose, pass the value through encodePathParam, the function the generated methods use:

import {encodePathParam} from 'deere-sdk';

const orgId = '123';
const fields = await client.get('fields', `/organizations/${encodePathParam('orgId', orgId)}/fields`);

encodeURIComponent on its own is not enough: it leaves . and .. as they are, and a URL treats those as navigation. encodePathParam refuses them, along with an empty value, and names the parameter in the error.

There is no baseUrl option. Each spec declares its own host, so the URL is resolved per call from the spec name and the environment. A spec served from fixed hosts throws UnsupportedEnvironmentError, before any request goes out, in an environment it has no host for. A spec served from {environment}.deere.com is resolved for any environment and never throws it. In HATEOAS mode the spec that gets resolved is the parent resource's, which changes this for some calls; see Strict Mode.

Where the access token is sent

The client attaches the OAuth Bearer token only to an https URL on *.deere.com. That holds for every request it makes: calls resolved from a spec name, pages followed by paginate, links followed in HATEOAS mode, and absolute URLs you pass to fetchUrl(method, url, body?, options?) or followLink(link). Any other URL you pass to those two is requested without an Authorization header.

// Bearer token attached (https on deere.com)
await client.fetchUrl('GET', 'https://api.deere.com/platform/organizations');

// Bearer token suppressed: the request still goes out, with no Authorization header
await client.fetchUrl('GET', 'https://example.com/some-webhook');

// To send a token elsewhere, pass your own Authorization header for that request
await client.fetchUrl('GET', 'https://example.com/api', undefined, {
    headers: { Authorization: 'Bearer some-other-token' },
});
  • An Authorization header in a request's options.headers is sent as given, to any host, and replaces the access token for that request. The header name is matched in any letter case.
  • An Authorization entry in defaultHeaders is ignored.
  • A link the client follows on its own, a HATEOAS link or a nextPage, must be an https URL on *.deere.com. Any other is not followed at all: the call throws (see Error Types). Following it without the token would still send the request there, with the headers you passed and, for a POST or PUT, its body.

Enable hateoasDebug: true to see a console.warn whenever a token is suppressed.


HATEOAS Mode

John Deere requires applications to demonstrate HATEOAS compliance before granting production API access. When enabled, the SDK automatically follows HAL links from parent resources instead of constructing URLs directly — producing the exact traffic pattern John Deere's certification requires.

import {Deere} from 'deere-sdk';

const deere = new Deere({
    accessToken: 'your-token',
    environment: 'sandboxapi',
    hateoas: true,       // Enable HATEOAS link traversal
    hateoasDebug: true,  // Optional: log resolution details
});

// Same API — HATEOAS resolution is transparent
const orgs = await deere.organizations.listAll();
const orgId = orgs[0]?.id;
if (!orgId) throw new Error('No organization available for this token');
const fields = await deere.fields.listAll(orgId);

When hateoas: true, the SDK fetches parent resources and follows their HAL links before accessing child resources. For example, deere.fields.list('org-123') first fetches /organizations/org-123, finds the fields link in its response, and uses that discovered URL. Link responses are cached per-session, so subsequent calls incur no extra latency.

Certification Walkthrough

import {Deere} from 'deere-sdk';

// 1. Create client with HATEOAS enabled
const deere = new Deere({
    accessToken: 'your-oauth-token',
    environment: 'sandboxapi',
    hateoas: true,
    hateoasDebug: true, // See link resolution in console
});

// 2. Warm the cache (optional — reduces cold-start latency)
await deere.client.warmLinkCache('organizations', ['/organizations']);

// 3. Use the SDK normally — HATEOAS happens transparently
const orgs = await deere.organizations.listAll();
const orgId = orgs[0]?.id;
if (!orgId) throw new Error('No organization available for this token');
const org = await deere.organizations.get(orgId);
const fields = await deere.fields.listAll(orgId);
const fieldId = fields[0]?.id;
if (!fieldId) throw new Error('This organization has no fields');
const boundaries = await deere.boundaries.listBoundaries(orgId, fieldId);

// Console output (with hateoasDebug: true):
// [HATEOAS] /organizations/org-123/fields → parent: /organizations/org-123, rel: fields → https://...
// [HATEOAS] /organizations/org-123/fields/f-456/boundaries → parent: /organizations/org-123/fields/f-456, rel: boundaries → https://...

// 4. After certification, disable HATEOAS for production speed
// hateoas: false (or omit — it's the default)

Strict Mode

HATEOAS mode is strict: if link resolution fails (the parent returns no matching link, the parent fetch errors, or the link is not an https URL on *.deere.com), a HateoasError is thrown immediately. This ensures certification compliance issues surface during development, not at John Deere's certification gate. The one exception is a call you have cancelled: it rejects with your signal's reason, as described under Timeouts and Cancellation.

import {HateoasError} from 'deere-sdk';

try {
    await deere.fields.listAll('org-123');
} catch (error) {
    if (error instanceof HateoasError) {
        console.log(`Link resolution failed: ${error.message}`);
        console.log(`Path: ${error.path}`);
    }
}

In HATEOAS mode the SDK builds only the parent's URL, so the per-API environment check runs against the parent's API, not the child's. When an API has no server for the configured environment:

  • A call whose parent belongs to another API is not refused. deere.equipment.create(orgId, ...) fetches the organization and sends the request to its equipment link.
  • A call whose parent belongs to that same API (deere.equipment.getEquipmenttypes(makeId)) throws HateoasError, with the UnsupportedEnvironmentError as its cause.
  • A call with no parent to follow (deere.equipment.get()) throws UnsupportedEnvironmentError, as it does without HATEOAS.

Error Handling

Automatic Retries

The SDK automatically retries failed requests with exponential backoff and jitter:

Error Type Retried? Notes
429 Rate Limit Yes Respects Retry-After header
500, 502, 503, 504 Yes Server errors
Network failures Yes Connection issues
Timeouts Yes Request took too long
401, 403 Auth errors No Refresh your token
400, 404, 422 No Fix your request
Cancelled by your signal No See below

The server-error, network and timeout rows apply to a POST or PATCH only while retryNonIdempotent is on, which is the default, and never once a success status has arrived for it. See Requests that change data.

Default behavior: 3 retries with exponential backoff (delays of ~1s, ~2s, ~4s with jitter).

import {Deere} from 'deere-sdk';

// Customize retry behavior
const patient = new Deere({
    accessToken: 'your-token',
    maxRetries: 5,  // More retries (default: 3)
});

// Disable retries entirely
const noRetries = new Deere({
    accessToken: 'your-token',
    maxRetries: 0,
});

Requests that change data

By default a POST or PATCH is retried like any other request. One that ends in a server error, a network failure or a timeout may still have been carried out, so sending it again can do the work twice.

import {Deere} from 'deere-sdk';

const careful = new Deere({
    accessToken: 'your-token',
    retryNonIdempotent: false,
});

With retryNonIdempotent: false, a POST or PATCH is retried only after a 429, on the understanding that a rate limiter turns a request away before it is handled. GET, PUT and DELETE are retried as before. The option defaults to true in 3.x; the default becomes false in 4.0.

Whatever the option says, a POST or PATCH is never sent again once a success status has arrived for it: if the timeout or a dropped connection interrupts the reading of that response's body, the call rejects with that error, because the server has already carried the request out.

Timeouts and Cancellation

Every generated method takes options as its last argument, and so does every method of the deere.safe facades, as do get, post, put, patch, delete, paginate, getAll, fetchUrl and followLink on DeereClient. Two of the options end a call early:

import {Deere, DeereError} from 'deere-sdk';

const deere = new Deere({accessToken: 'your-token'});
const controller = new AbortController();
// Call controller.abort() from anywhere to cancel, e.g. when the user leaves the page.

try {
    const fields = await deere.fields.listAll('org-id', undefined, {
        signal: controller.signal,
        timeout: 10000, // for this call; the client's `timeout` is the default
    });
    console.log(fields.length);
} catch (error) {
    if (controller.signal.aborted) {
        // Cancelled. `error` is controller.signal.reason, an AbortError by default.
    } else if (error instanceof DeereError && error.statusText === 'Timeout') {
        // The last attempt did not finish in time, its response body included.
    } else {
        throw error;
    }
}
  • timeout is how long each attempt may take, from sending the request to reading the whole response body. An attempt that times out is retried like any other transient failure, except a POST or PATCH whose success status had already arrived (see Requests that change data). When the timeout cuts short the body of an error response, the call rejects with that HTTP error, whose body is then missing. If the last attempt is one that timed out, the call rejects with a DeereError whose status is 0 and statusText is 'Timeout'. In HATEOAS mode, when it was the request for the parent resource that timed out, that error arrives as the cause of a HateoasError. Infinity turns the timeout off. A timeout must be a number of milliseconds greater than 0: the constructor throws for 0, a negative number, NaN or anything that is not a number, and a call given such a value in options rejects before sending anything (see Error Types). A numeric string such as '30000' is accepted.
  • signal cancels the call. The call rejects with the signal's reason: an AbortError by default, or whatever you passed to abort(). That is what fetch does, so the error is not a DeereError. A cancelled call is never retried, a wait before a retry ends at once, and paginate, getAll and HATEOAS mode request nothing further.
  • The two apply together. Passing a signal does not switch the timeout off.
  • A custom fetch must honour the signal it is handed. If it does not, neither a timeout nor a cancellation can interrupt a request already in flight.

Error Types

Error When it's thrown Retried?
DeereError Base class. Any HTTP error from John Deere (400/404/422/5xx). Only 5xx
RateLimitError HTTP 429. Only reaches your catch after all retries are exhausted. Yes
AuthError HTTP 401/403. Refresh your OAuth token. No
DeereError with status 0 and statusText 'Timeout' The last attempt did not finish within timeout, reading the response body included. Yes
DeereError with status -1 and statusText 'InvalidTimeout' The timeout in a call's options is not a number of milliseconds greater than 0. Thrown before any request goes out. No
The reason of your signal (an AbortError by default) You aborted the signal you passed in options. Not a DeereError. See Timeouts and Cancellation. No
HateoasError HATEOAS link resolution fails (parent fetch errored, no matching link, or the link is not an https URL on *.deere.com). No
DeereError with status -1 and statusText 'UntrustedLink' paginate or getAll met a nextPage link that is not an https URL on *.deere.com. The link is not requested. No
UnsupportedEnvironmentError A call targets an API served from fixed hosts, and it has none for the configured environment. Thrown before any request goes out. An API served from {environment}.deere.com never throws it. With hateoas: true, see Strict Mode. No
NoServerConfigError A call targets a spec with no usable servers block. Thrown before any request goes out. No
DeereError with status -1 and statusText 'InvalidPathParameter' A path parameter, given to a generated method or to encodePathParam, cannot be one path segment: it is not a string, a finite number, a bigint or a boolean, or it is an empty string, ., .. or malformed text. Thrown before any request goes out. No
import {
    DeereError,
    RateLimitError,
    AuthError,
    HateoasError,
    UnsupportedEnvironmentError,
} from 'deere-sdk';

try {
    const fields = await deere.fields.listAll('org-id');
} catch (error) {
    if (error instanceof RateLimitError) {
        // Only thrown after all retries exhausted
        console.log(`Rate limited. Retry after ${error.retryAfter}s`);
    } else if (error instanceof AuthError) {
        // Never retried - refresh your token
        console.log('Token expired - refresh required');
    } else if (error instanceof HateoasError) {
        // HATEOAS link resolution failed (only when hateoas: true)
        console.log(`Link resolution failed on ${error.path}: ${error.message}`);
    } else if (error instanceof UnsupportedEnvironmentError) {
        // Spec doesn't ship this environment — try another from its valid list
        console.log(error.message);
    } else if (error instanceof DeereError) {
        console.log(`API error: ${error.status} ${error.message}`);
    }
}

TypeScript Support

Access auto-generated types from OpenAPI specs:

import {Types} from 'deere-sdk';

type Farm = Types.Farms.components['schemas']['GetFarm'];
type Field = Types.Fields.components['schemas']['FieldsResponse'];
type Equipment = Types.Equipment.components['schemas']['equipment-model'];

API Status

This SDK includes automated daily checks against John Deere's live infrastructure. One asks whether every spec is still published and consumable; the other asks whether every URL the SDK builds still corresponds to a route that exists.

Status Meaning
passing Every spec is fresh from John Deere's portal and validates
degraded The only shortfall is specs deliberately frozen with a recorded reason and date
failing An active spec cannot be fetched or validated, so the pipeline is blind to it

Yellow is not a partial outage. A frozen spec is one this repo deliberately pins to a committed copy rather than the portal's current document, usually because adopting the current one would drop a method that still works. scripts/spec-registry.yaml records which specs are frozen, why, and since when. Green means nothing is pinned.

APIs Without Public Specs

These APIs are listed on John Deere's portal but don't provide public OpenAPI specs:

API Notes
work-plans Listed but returns empty spec
retrieve-warranty-information Dealer-only
retrieve-pip Dealer-only
valid-pin Dealer-only
Additional John Deere APIs

John Deere offers 40+ additional APIs not included in this SDK:

  • Dealer Solutions (32 APIs) — Warranty, quotes, service for dealers
  • Financial (4 APIs) — Merchant transactions, credit applications
  • Supply Chain (1 API) — Supplier quoting

These target dealers rather than farmers. See developer.deere.com for full documentation.


Versioning

The SDK is regenerated from John Deere's published specs by a daily job. What that job may release without a person looking is limited:

  • Pinned: which endpoint each method calls, and each method's parameters and return type. A new operation is a minor release. A method that would lose its endpoint, or whose parameters or return type would change, stops the job; the release that follows is numbered for what the change does to your code.
  • Not pinned: what a schema type contains. When John Deere adds, drops or retypes a field of a type under Types.<Api>.components['schemas'], the regenerated types ship as a patch, even where code that relied on the old shape stops compiling.

Contributing

Contributions are welcome! Please read the contributing guidelines before submitting a PR.

# Clone the repo
git clone https://github.com/ProductOfAmerica/deere-sdk.git

# Install dependencies
pnpm install

# Build
pnpm build

# Run tests
pnpm test

# After changing a generator in scripts/: regenerate from the committed specs
pnpm redact-specs && pnpm fix-specs && pnpm generate-api-servers && pnpm generate-types && pnpm generate-sdk && pnpm lint:fix

Regenerate with those stages, not with pnpm generate. That one starts by fetching John Deere's current specs, and upstream changes are meant to arrive through the daily sync, which classifies them and picks the version. If your change alters a method's parameters or return type, pnpm generate-sdk stops and lists the methods; pnpm accept-signatures re-pins them once you have checked the list is what you meant.

Releasing

Normal development: just commit and push as usual. No release happens:

git add -A
git commit -m "fix: whatever you fixed"
git push

When ready to release a new version:

# 1. Update CHANGELOG.md with what changed, and commit it
#    (npm version refuses to run with uncommitted changes to tracked files)

# 2. Bump version: runs the checks, then commits and tags
npm version patch   # or: minor (a major is below)

# 3. Push commit and tag together
git push --follow-tags

After writing the new version into package.json, npm version runs the version script (pnpm typecheck:test && pnpm test, the checks the release workflow runs) and commits and tags only if it passes. A failure leaves package.json bumped and uncommitted, with no commit and no tag. To recover, run git restore package.json, commit the fix, and run npm version again; until package.json is restored, npm version refuses to run. The daily sync bumps the version with --ignore-scripts, because it has just run the same checks and never bumps a major.

A major release can need a change that the tests require at the new major (for example deleting a relocation whose removeInMajor is reached). Keep that change out of main until the bump, since a daily sync could otherwise release it as a minor or a patch, and commit it together with the bump:

# make the change, regenerate, update CHANGELOG.md; leave it all uncommitted
npm version major --no-git-tag-version   # bumps and runs the checks on this tree, touches no git state
git add -A
git commit -m "X.0.0"
git tag -a vX.0.0 -m "X.0.0"             # must be annotated: --follow-tags pushes annotated tags only
git push --follow-tags

This commits exactly the tree the checks passed. If they fail, nothing is committed: run git restore package.json before running the command again, or it bumps a second time (4.0.0 to 5.0.0).

CI sees the tag → type-checks and tests that commit → creates GitHub Release → publishes to npm. If a test fails, no release is created and nothing is published.

npm publishing uses Trusted Publishing/OIDC via release.yml; do not create or restore an NPM_TOKEN for CI publishing.

Gotcha: GitHub sometimes does not fire the push event for a tag pushed via git push --follow-tags, so release.yml never auto-triggers. If you don't see a "Create Release" run within a minute of pushing the tag, dispatch it manually:

gh workflow run release.yml --ref vX.Y.Z

release.yml also accepts workflow_dispatch, so this produces an identical run against the tag's commit.


Verifying Releases

Every release produces two independently verifiable artifacts, and they are not byte-identical: the package on the npm registry (built and published by publish.yml) and the .tgz attached to the GitHub Release page (built by release.yml). They verify differently. npm audit signatures checks the copy you installed from the registry; it does not verify a tarball downloaded from the Release page. Both chains trace back to Sigstore through GitHub's OIDC identity, so there is no separate signing key to manage.

The npm package (what npm install pulls)

npm audit signatures

Verifies the registry signature and the SLSA build-provenance attestation for your installed packages, including deere-sdk.

The GitHub Release tarball (the .tgz on the Release page)

gh release download vX.Y.Z --repo ProductOfAmerica/deere-sdk --pattern '*.tgz'
gh attestation verify deere-sdk-X.Y.Z.tgz \
  --repo ProductOfAmerica/deere-sdk \
  --signer-workflow ProductOfAmerica/deere-sdk/.github/workflows/release.yml \
  --source-ref refs/tags/vX.Y.Z

--source-ref binds the check to the release tag. --predicate-type defaults to https://slsa.dev/provenance/v1, so it can be omitted. By default gh attestation verify fetches the attestation from GitHub's API (online).

Offline / air-gapped / mirrors

Fetch the Sigstore bundle once while online, then verify with no further API calls:

gh attestation download deere-sdk-X.Y.Z.tgz --repo ProductOfAmerica/deere-sdk
gh attestation verify deere-sdk-X.Y.Z.tgz \
  --bundle <downloaded-bundle>.jsonl \
  --repo ProductOfAmerica/deere-sdk \
  --signer-workflow ProductOfAmerica/deere-sdk/.github/workflows/release.yml \
  --source-ref refs/tags/vX.Y.Z

Disclaimer

This is an unofficial SDK and is not affiliated with, endorsed by, or connected to John Deere or Deere & Company. Use at your own risk.

John Deere, Operations Center, and the leaping deer logo are trademarks of Deere & Company.


License

MIT © 2026


Built with TypeScript and auto-generated from John Deere OpenAPI specs

About

Unofficial TypeScript SDK for John Deere Operations Center API

Topics

Resources

Contributing

Security policy

Stars

13 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages