Enterprise plan
DrillerDB Public API v1
Read your own operational data: projects, invoices, work orders, contacts, equipment, and inventory. With a write scope an admin grants, create and edit contacts, projects and work orders, and create estimate drafts. The API never sends an email, never moves money, and never sends an estimate.
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:readcontacts:writeequipment:readestimates:readestimates:writeinventory:readinvoices:readprojects:readprojects:writework_orders:readwork_orders:write
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 (19)
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 |
| POST /projects | projects:write | none |
| PATCH /projects/{intWellKey} | projects:write | 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 |
| POST /work-orders | work_orders:write | none |
| PATCH /work-orders/{id} | work_orders:write | none |
Contacts
| Endpoint | Scope | Query parameters |
|---|---|---|
| GET /contacts | contacts:read | limit, cursor, updated_since |
| GET /contacts/{rolodexId} | contacts:read | none |
| POST /contacts | contacts:write | none |
| PATCH /contacts/{rolodexId} | contacts:write | 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 |
Estimates
| Endpoint | Scope | Query parameters |
|---|---|---|
| POST /projects/{intWellKey}/estimates | estimates:write | none |
| GET /estimates/{token} | estimates:read | none |
Resources (7)
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}, POST /projects, PATCH /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}, POST /work-orders, PATCH /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}, POST /contacts, PATCH /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 |
Estimate
POST /projects/{intWellKey}/estimates, GET /estimates/{token}
| Field | Type | Notes |
|---|---|---|
| id | integer | |
| token | string | |
| project_id | integer | |
| status | string | draft until an office user sends it from DrillerDB. |
| total | number, nullable | |
| sent | boolean | |
| portal_url | string, nullable | |
| created_at | string, nullable |
Writes
A write needs a :write scope on the key. Only an admin can create a key with one, in Settings, API keys. The key then acts as its own service identity: every record it writes is attributed to that identity, which can never sign in.
Every POST needs an Idempotency-Key header of 1 to 255 characters. Retrying with the same key and the same body returns the first response and sets Idempotent-Replayed: true. The same key with a different body answers 409 IDEMPOTENCY_KEY_REUSED.
Bodies are strict: an unknown field is refused with 422 VALIDATION_FAILED. Your company always comes from the key, never from the body. An id in a body (a contact, project or user) must belong to your company, or the write is refused with 422 REFERENCE_NOT_FOUND and nothing is written.
An estimate created here is a draft. Nothing is emailed or texted; an office user reviews and sends it from DrillerDB.
Request bodies (7)
CreateContactRequest
POST /contacts
| Field | Type | Notes |
|---|---|---|
| first_name | string | |
| last_name | string | |
| business_name | string | |
| string | ||
| phone | string | |
| cell_phone | string | |
| fax | string | |
| address | string | |
| city | string | |
| state | string | |
| zip | string | |
| type | string | One of customer, vendor, subcontractor. |
| notes | string |
UpdateContactRequest
PATCH /contacts/{rolodexId}
| Field | Type | Notes |
|---|---|---|
| first_name | string, nullable | |
| last_name | string, nullable | |
| business_name | string, nullable | |
| string, nullable | ||
| billing_email | string, nullable | |
| phone | string, nullable | |
| cell_phone | string, nullable | |
| fax | string, nullable | |
| address | string, nullable | |
| address2 | string, nullable | |
| city | string, nullable | |
| state | string, nullable | |
| zip | string, nullable | |
| website | string, nullable | |
| notes | string, nullable |
CreateProjectRequest
POST /projects
| Field | Type | Notes |
|---|---|---|
| address | string | |
| city | string | |
| state | string | |
| zip | string | |
| project_title | string | |
| customer_contact_id | integer | |
| customer_type | string | One of homeowner, contractor, pump_installer. |
| well_type | string | |
| drill_type | string | |
| notes | string |
UpdateProjectRequest
PATCH /projects/{intWellKey}
| Field | Type | Notes |
|---|---|---|
| project_title | string | |
| well_type | string | |
| drill_type | string | |
| address | string | |
| city | string | |
| state | string | |
| zip | string | |
| gps_latitude | string | |
| gps_longitude | string | |
| customer_contact_id | integer | |
| customer_type | string | One of homeowner, contractor, pump_installer. |
| append_note | string |
CreateWorkOrderRequest
POST /work-orders
| Field | Type | Notes |
|---|---|---|
| project_id | integer | |
| customer_contact_id | integer | |
| category | string | |
| work_detail | string | |
| field_tech_user_id | string | |
| site_address | string | |
| site_city | string | |
| site_state | string | |
| site_zip | string |
UpdateWorkOrderRequest
PATCH /work-orders/{id}
| Field | Type | Notes |
|---|---|---|
| customer_contact_id | integer, nullable | |
| category | string | |
| work_detail | string | |
| field_tech_user_id | string, nullable |
CreateEstimateRequest
POST /projects/{intWellKey}/estimates
| Field | Type | Notes |
|---|---|---|
| line_items | array | |
| proposal_name | string | |
| proposal_date | string | |
| validity_days | integer |
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. |
| 400 | INVALID_JSON | A write request body is not valid JSON, or is larger than 256 KB. |
| 400 | IDEMPOTENCY_KEY_REQUIRED | A POST carried no Idempotency-Key header (1 to 255 characters). |
| 403 | WRITE_ACTOR_MISSING | The key holds a write scope but no write identity. Create the key with its write scope in Settings > API keys. |
| 409 | IDEMPOTENCY_KEY_REUSED | The Idempotency-Key was already used with a different request body or path. |
| 409 | IDEMPOTENCY_IN_PROGRESS | A request with the same Idempotency-Key is still running. Retry after it finishes. |
| 409 | WRITE_CONFLICT | The write conflicts with existing data, for example an estimate that already has line items, or a project past Work Order status. |
| 422 | VALIDATION_FAILED | The body failed validation. Unknown fields are refused. meta.issues lists each problem. |
| 422 | REFERENCE_NOT_FOUND | An id in the body (contact, project, user) does not belong to your company. meta.field names it. |
| 503 | FEATURE_UNAVAILABLE | API writes are not enabled on this deployment yet. |
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.