Enterprise plan
DrillerDB Public API v1
Read your own operational data: projects, invoices, work orders, contacts, equipment, and inventory. Every endpoint is a GET. The API writes nothing.
Base URL
Every request starts with this base:
https://console.drillerdb.com/api/partner/v1
The links.self and links.next values in each response are paths, not full URLs. Prepend the host above to follow them. The OpenAPI document reports the same prefix as x-link-prefix.
Authentication
Send your key in the X-API-Key header on every request.
curl -H "X-API-Key: ddb_live_example.your_secret_here" \ "https://console.drillerdb.com/api/partner/v1/projects?limit=50"
A company administrator creates keys in the console under Settings, Automation, API Keys. The company must be on the Enterprise plan. Keys start with ddb_live_, belong to one company, and can be revoked at any time. The secret is shown once, when the key is created.
Who can use it
The API is available to companies on the Enterprise plan. A key belonging to any other plan returns 403 ENTERPRISE_PLAN_REQUIRED, even when the key itself is valid.
Each key carries its own scopes. A request to an endpoint outside the key's scopes returns 403 INSUFFICIENT_SCOPE. The scopes are:
contacts:readequipment:readinventory:readinvoices:readprojects:readwork_orders:read
Invoices sit under a separate scope from the projects that own them, so a key can read projects without reading their money.
Rate limits
Each key has a per-minute limit and a per-day limit, both set when the key is created. A new key defaults to 120 requests per minute and 5,000 per day.
When a key passes either limit, the API returns 429 RATE_LIMITED with a Retry-After header giving the wait in seconds. Wait that long before retrying.
Endpoints (11)
Every endpoint returns data for the company that owns the key. There is no way to read another company's records.
Projects
| Endpoint | Scope | Query parameters |
|---|---|---|
| GET /projects | projects:read | limit, cursor, updated_since |
| GET /projects/{intWellKey} | projects:read | none |
Invoices
| Endpoint | Scope | Query parameters |
|---|---|---|
| GET /projects/{intWellKey}/invoices | invoices:read | limit, cursor |
Work Orders
| Endpoint | Scope | Query parameters |
|---|---|---|
| GET /work-orders | work_orders:read | limit, cursor |
| GET /work-orders/{id} | work_orders:read | none |
Contacts
| Endpoint | Scope | Query parameters |
|---|---|---|
| GET /contacts | contacts:read | limit, cursor, updated_since |
| GET /contacts/{rolodexId} | contacts:read | none |
Equipment
| Endpoint | Scope | Query parameters |
|---|---|---|
| GET /equipment | equipment:read | limit, cursor, updated_since |
| GET /equipment/{equipmentId} | equipment:read | none |
Inventory
| Endpoint | Scope | Query parameters |
|---|---|---|
| GET /inventory/items | inventory:read | limit, cursor, updated_since |
| GET /inventory/items/{id} | inventory:read | none |
Resources (6)
Each resource below is the data value an endpoint returns: one object for an item endpoint, an array of them for a list endpoint. The tables come from the same schemas the OpenAPI document publishes. A field marked nullable can be null.
Project
GET /projects, GET /projects/{intWellKey}
| Field | Type | Notes |
|---|---|---|
| id | integer | |
| project_number | string | |
| title | string | |
| status | string | |
| priority | integer, nullable | |
| customer_id | integer, nullable | |
| customer_name | string | |
| address | object | |
| address.line1 | string, nullable | |
| address.line2 | string, nullable | |
| address.city | string, nullable | |
| address.state | string, nullable | |
| address.postal_code | string, nullable | |
| drill_type | string, nullable | |
| well_type | string, nullable | |
| created_by | string, nullable | |
| created_at | string, nullable | |
| updated_at | string, nullable |
Invoice
GET /projects/{intWellKey}/invoices
| Field | Type | Notes |
|---|---|---|
| id | integer | |
| invoice_number | string | |
| status | string | Document lifecycle state as stored (draft, sent, paid, void, ...). |
| payment_status | string | One of open, partial, paid. Derived from the invoice total and recorded payments, not from a stored column. |
| invoice_date | string, nullable | |
| due_date | string, nullable | |
| due_terms_raw | string, nullable | The stored customer_invoices.due_date string, which may be a date or payment terms such as "30" or "receipt". due_date carries the resolved date. |
| invoice_total_cents | integer, nullable | |
| balance | number, nullable | Amount still owed, derived alongside payment_status. |
| paid_amount | number, nullable | |
| created_at | string, nullable | |
| paid_at | string, nullable | |
| last_sent_at | string, nullable | |
| project_id | integer, nullable |
WorkOrder
GET /work-orders, GET /work-orders/{id}
| Field | Type | Notes |
|---|---|---|
| id | integer | |
| project_id | integer, nullable | |
| status | string | |
| category | string | |
| work_type | string, nullable | |
| field_tech | string, nullable | |
| service_date | string, nullable | |
| completed_at | string, nullable | |
| customer_id | integer, nullable | |
| customer_name | string | |
| project_title | string, nullable | |
| memo | string, nullable | |
| amount_billed | string | number, nullable |
Contact
GET /contacts, GET /contacts/{rolodexId}
| Field | Type | Notes |
|---|---|---|
| id | integer | |
| business_name | string, nullable | |
| first_name | string, nullable | |
| last_name | string, nullable | |
| string, nullable | ||
| billing_email | string, nullable | |
| phone | string, nullable | |
| cell | string, nullable | |
| address | object | |
| address.line1 | string, nullable | |
| address.line2 | string, nullable | |
| address.city | string, nullable | |
| address.state | string, nullable | |
| address.postal_code | string, nullable | |
| is_client | boolean | |
| is_vendor | boolean | |
| is_active | boolean | |
| updated_at | string, nullable |
Equipment
GET /equipment, GET /equipment/{equipmentId}
| Field | Type | Notes |
|---|---|---|
| id | string | |
| name | string | |
| make | string, nullable | |
| model | string, nullable | |
| year | integer, nullable | |
| status | string, nullable | |
| category | string, nullable | |
| assigned_employee_id | string | integer, nullable | |
| last_location | object | |
| last_location.latitude | number, nullable | |
| last_location.longitude | number, nullable | |
| last_location.updated_at | string, nullable | |
| updated_at | string, nullable | |
| created_at | string, nullable |
InventoryItem
GET /inventory/items, GET /inventory/items/{id}
| Field | Type | Notes |
|---|---|---|
| id | integer | |
| item_id | string | |
| name | string | |
| description | string, nullable | |
| category | string, nullable | |
| current_quantity | number, nullable | |
| min_quantity | number, nullable | |
| max_quantity | number, nullable | |
| unit | string, nullable | |
| unit_price | number, nullable | |
| location | string, nullable | |
| sku | string, nullable | |
| is_active | boolean, nullable | |
| last_restocked_date | string, nullable | |
| created_at | string, nullable | |
| updated_at | string, nullable |
Pagination
List endpoints return a page of records and an opaque cursor. Read meta.next_cursor and send it back as the cursor parameter to get the next page. When meta.has_more is false, you have read everything.
{
"data": [ ... ],
"meta": {
"request_id": "req_0f3a...",
"limit": 50,
"has_more": true,
"next_cursor": "eyJ1cGRhdGVkX2F0Ijoi..."
},
"links": { "self": "...", "next": "..." }
}limit defaults to 50 and caps at 200. Do not build the cursor yourself. Its contents are not part of the contract and can change.
Endpoints that accept updated_since return only records changed at or after the timestamp you give. The endpoint table above says which ones accept it. Use it to sync changes instead of reading every page each time.
Errors
The HTTP status is authoritative. Every response carries meta.request_id. Quote that value when you report a problem.
{
"error": { "code": "INSUFFICIENT_SCOPE", "message": "..." },
"meta": { "request_id": "req_0f3a..." },
"links": { "self": "..." }
}| Status | Code | Meaning |
|---|---|---|
| 401 | API_KEY_REQUIRED | The request carried no X-API-Key header. |
| 401 | INVALID_API_KEY | No key matches the value sent. |
| 401 | API_KEY_INACTIVE | The key exists but is revoked. |
| 401 | API_KEY_EXPIRED | The key passed the expiry date set when it was created. |
| 403 | ENTERPRISE_PLAN_REQUIRED | The company is not on the Enterprise plan. |
| 403 | INSUFFICIENT_SCOPE | The key does not hold the scope this endpoint requires. |
| 429 | RATE_LIMITED | The key passed its per-minute or per-day limit. The Retry-After header gives the wait in seconds. |
| 500 | INTERNAL_ERROR | The request failed for a reason the API does not disclose. |
| 503 | API_NOT_READY | The API schema is unavailable. meta.missing_tables names what is absent. |
A record that does not exist and a record belonging to another company both return 404, with the same body. The API does not confirm that an identifier exists elsewhere.
OpenAPI document
The machine-readable contract is generated from the same route definitions this page is generated from, so the two cannot disagree. It needs no key.
https://console.drillerdb.com/api/partner/v1/openapi.json
Open the OpenAPI document. It is OpenAPI 3.1. Import it into Postman, or generate a client from it.
Notes on the data
Work orders are ordered by identifier and accept no updated_since. The underlying records carry no reliable change timestamp.
On invoices, status is the document state as stored, such as draft, sent, or void. payment_status and balance are calculated from the invoice total and the payments recorded against it, so they report what is actually owed.
Equipment identifiers are strings. Every other resource uses an integer identifier.