Unofficial TypeScript SDK for John Deere Operations Center API
Installation • Quick Start • API Reference • API Status
- 28 APIs with 146 operations — 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 API availability
npm install deere-sdkpnpm add deere-sdkyarn add deere-sdkimport {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();
// Get fields for an organization
const fields = await deere.fields.listAll(orgs[0].id);
// Get equipment
const equipment = await deere.equipment.get();This SDK requires an OAuth 2.0 access token from John Deere:
- Register at developer.deere.com
- Create an application and get your client ID/secret
- Implement the OAuth 2.0 authorization code flow
- 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
v2.0.0 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 spec has servers for every environment. If you pick an environment a given spec doesn't ship (e.g., a production-only spec on sandboxapi), the constructor throws UnsupportedEnvironmentError with the valid values for that spec.
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.
| API | Property | Methods | Description |
|---|---|---|---|
| Organizations | deere.organizations |
5 | 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 |
4 | Harvests, plantings, applications |
| Crop Types | deere.cropTypes |
5 | Crop type catalog |
| Products | deere.products |
10 | Seeds and chemicals catalog |
| Map Layers | deere.mapLayers |
5 | Map layer management |
| Files | deere.files |
6 | File management |
| Flags | deere.flags |
7 | Field flags/markers |
| Guidance Lines | deere.guidanceLines |
5 | GPS guidance lines |
| Operators | deere.operators |
7 | Machine operator management |
| Users | deere.users |
1 | User information |
| Assets | deere.assets |
9 | Asset tracking |
| Webhooks | deere.webhook |
5 | Event subscriptions |
| Connections | deere.connectionManagement |
4 | OAuth connections |
| API | Property | Methods | Description |
|---|---|---|---|
| Machine Locations | deere.machineLocations |
1 | 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 |
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');
// List users in an organization
const users = await deere.organizations.listUsers('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
// 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');
// CRUD
await deere.equipment.create('org-id', {type: 'Machine', name: 'Tractor 1'});
await deere.equipment.update('equipment-id', {name: 'Updated'});
await deere.equipment.delete('equipment-id');
// Reference data
const makes = await deere.equipment.list();
const types = await deere.equipment.listEquipmenttypes();
const models = await deere.equipment.listEquipmentmodels({equipmentModelName: '9RX*'});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
await deere.notifications.create({
sourceEvent: 'my-app-event-123',
title: 'Action Required',
message: 'Please review the prescription map'
});
// 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'
});
await deere.assets.createLocations('asset-id', {
timestamp: '2026-01-15T12:00:00Z',
geometry: {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
});For custom endpoints or advanced use cases:
import {DeereClient} from 'deere-sdk';
const client = new DeereClient({
accessToken: 'your-token',
environment: 'sandboxapi',
timeout: 30000, // Request timeout in ms (default: 30000)
maxRetries: 3, // Retry attempts (default: 3, set to 0 to disable)
});
// Raw requests
const response = await client.get<CustomType>('/some/endpoint');
const created = await client.post('/some/endpoint', {data: 'value'});
// Follow HAL links
const nextPage = await client.followLink(response.links[0]);
// Automatic pagination
for await (const items of client.paginate('/large/collection')) {
console.log(items);
}DeereClient exposes fetchUrl(method, url, body?, options?) and followLink(url) for calling absolute URLs (e.g., HAL next page links, externally-discovered URLs). Both suppress the OAuth Bearer token on any URL whose hostname is not a trusted *.deere.com origin. This prevents accidental token leakage if a HATEOAS response, user-supplied URL, or link cache ever includes a third-party host.
// Bearer token attached (trusted host)
await client.fetchUrl('GET', 'https://api.deere.com/platform/organizations');
// Bearer token SUPPRESSED (untrusted host) — request still goes out, just without Authorization
await client.fetchUrl('GET', 'https://example.com/some-webhook');
// You can opt in explicitly by passing your own Authorization header:
await client.fetchUrl('GET', 'https://example.com/api', undefined, {
headers: { Authorization: 'Bearer some-other-token' },
});Enable hateoasDebug: true to see a console.warn whenever a token is suppressed, so you can audit where your HATEOAS graph is pointing.
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.
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 fields = await deere.fields.listAll(orgs[0].id);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.
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']);
// 3. Use the SDK normally — HATEOAS happens transparently
const orgs = await deere.organizations.listAll();
const org = await deere.organizations.get(orgs[0].id);
const fields = await deere.fields.listAll(org.id);
const boundaries = await deere.boundaries.listBoundaries(org.id, fields[0].id);
// 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)HATEOAS mode is strict: if link resolution fails (parent returns no matching link, parent fetch errors), a HateoasError is thrown immediately. This ensures certification compliance issues surface during development, not at John Deere's certification gate.
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}`);
}
}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 |
Default behavior: 3 retries with exponential backoff (delays of ~1s, ~2s, ~4s with jitter).
// Customize retry behavior
const deere = new Deere({
accessToken: 'your-token',
maxRetries: 5, // More retries (default: 3)
});
// Disable retries entirely
const deere = new Deere({
accessToken: 'your-token',
maxRetries: 0,
});| 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 |
HateoasError |
HATEOAS link resolution fails (parent fetch errored or no matching link). | No |
UnsupportedEnvironmentError |
Constructor threw because environment doesn't match the spec's servers block. |
N/A (thrown at construction) |
NoServerConfigError |
Constructor threw because a spec has no servers block at all. |
N/A |
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}`);
}
}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'];This SDK includes automated daily health checks to monitor John Deere API availability.
| Status | Meaning |
|---|---|
| All APIs responding with valid specs | |
| Some APIs unavailable or returning empty specs | |
| Major API outage detected |
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.
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
# Fetch specs and generate SDK
pnpm generate
# Build
pnpm build
# Run tests
pnpm testNormal development – just commit and push as usual. No release happens:
git add -A
git commit -m "fix: whatever you fixed"
git pushWhen ready to release a new version:
# 1. Update CHANGELOG.md with what changed
# 2. Bump version (auto-commits + auto-creates tag)
npm version patch # or: minor, major
# 3. Push commit and tag together
git push --follow-tagsCI sees the tag → creates GitHub Release → publishes to npm.
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.Zrelease.yml also accepts workflow_dispatch, so this produces an identical run against the tag's commit.
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.
MIT © 2026
Built with TypeScript and auto-generated from John Deere OpenAPI specs
